Skip to content

CVE-2026-39980: Verified Reproduction

CVE-2026-39980: OpenCTI CVE-2026-39980 safeEjs destructuring fix bypass RCE

CVE-2026-39980 is verified against opencti-platform/opencti · github. Vulnerability class: RCE. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00331.

REPRO-2026-00331 opencti-platform/opencti · github RCE Aug 23, 2026 CVE entry .txt
Severity
CRITICAL
Confidence
HIGH
Reproduced in
106m 24s
Tool calls
244
Spend
$5.64
01 · Overview

What Is CVE-2026-39980?

CVE-2026-39980 is a critical-severity RCE vulnerability affecting opencti-platform/opencti. Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00331).

02 · Severity & CVSS

CVE-2026-39980 Severity

CVE-2026-39980 is rated critical severity.

CRITICAL threat level
Weakness CWE-94 — Improper Control of Generation of Code ('Code Injection')

Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.

How to Reproduce CVE-2026-39980

$ pruva-verify REPRO-2026-00331
or curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00331/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-39980

Remote code execution — reproduced
  • reached the target end-to-end
  • full exploit chain demonstrated
  • on the real production code path
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

JsonMapper variable formula in jsonMapperTest GraphQL mutation: (()=>{const{"constructor":F}=Array;return F(<attacker JS>)()})() - quoted PatternProperty key bypasses safeEjs denylist

Attack chain
  1. POST /graphql jsonMapperTest multipart mutation
  2. jsonMapper-domain.ts
  3. parser/json-mapper.ts extractComplexPathFromJson
  4. safeEjs.ts safeRender
  5. Function constructor
  6. child_process.execSync (root); pre-auth via CVE-2026-27960 Bearer admin internal_id
How the agent worked 508 events · 244 tool calls · 1h 46m
1h 46mDuration
244Tool calls
82Reasoning steps
508Events
24Dead-ends
Agent activity over 1h 46m
Policy
1
Support
5
Repro
274
Judge
29
Variant
194
Verify
1
0:00106:13

Root Cause and Exploit Chain for CVE-2026-39980

Versions: 6.9.5 (the CVE-2026-39980 "fixed" release). The auth bypass used for pre-auth reachability (CVE-2026-27960) is live on 6.9.5 and was only fixed in 6.9.13; even without it, any authenticated user with the CSVMAPPERS capability (or access to notifier template testing, which shares the transformTemplate core) can trigger the same sandbox escape.

OpenCTI 6.9.5 shipped a fix for CVE-2026-39980 (commit d91c19e1e7, "[backend] Safe ejs with verifier") that rewrites safeRender in src/utils/safeEjs.ts with an @lezer/javascript AST verifier plus a runtime property guard (____safe____property). The fix contains a static-analysis gap: quoted object keys are checked against the forbiddenProperties denylist only when their AST parent is an object-literal Property node. Quoted keys in destructuring patterns are PatternProperty children and are never inspected, and because destructuring performs a real [[Get]] without bracket tokens, the runtime guard (which is injected only around [ ]) is never applied either. The formula (()=>{const{"constructor":F}=Array;return F(<attacker JS>)()})() therefore retrieves Function from the allowed Array global and executes arbitrary JavaScript inside the OpenCTI Node.js process. Delivered through the jsonMapperTest GraphQL mutation (JsonMapper variable formula) and chained with the still-unpatched CVE-2026-27960 Bearer-UUID auth bypass, this yields unauthenticated remote code execution as root on a fully patched-for-CVE-2026-39980 OpenCTI 6.9.5.

  • Package/component affected: opencti/platform (backend opencti-graphql), file src/utils/safeEjs.ts (processString / transformTemplate).
  • Affected versions: 6.9.5 (the CVE-2026-39980 "fixed" release). The auth bypass used for pre-auth reachability (CVE-2026-27960) is live on 6.9.5 and was only fixed in 6.9.13; even without it, any authenticated user with the CSVMAPPERS capability (or access to notifier template testing, which shares the transformTemplate core) can trigger the same sandbox escape.
  • Risk level and consequences: Critical. Unauthenticated remote code execution as uid=0(root) inside the platform container — full platform compromise (all threat-intel data, credentials, connected systems).

Impact Parity

  • Disclosed/claimed maximum impact: code execution (root RCE, pre-auth when chained with CVE-2026-27960).
  • Reproduced impact from this run: full remote code execution as uid=0(root) inside two fresh opencti/platform:6.9.5 containers per pass, via the real POST /graphql jsonMapperTest endpoint, preceded by remote proof of the CVE-2026-27960 auth bypass (me query returns the admin identity when only Authorization: Bearer 88ec0c6a-13ce-5e39-b486-354fe4a7084f is supplied).
  • Parity: full.
  • Not demonstrated: nothing claimed was left undemonstrated. (Persistence/exfiltration beyond the marker command was not attempted and was not claimed.)

Root Cause

src/utils/safeEjs.ts (tag 6.9.5 = commit be4ab13c30d154adc3cfc49ba128b2039b93e348, fix commit d91c19e1e7 contained):

const processString = () => {
  const parentType = cursor.node.parent?.type.name;
  if (parentType === 'Property') {        // object literals ONLY
    processPropertyDefinitionOrName();    // forbiddenProperties denylist check
  }
};
  1. In @lezer/javascript, a quoted key in an object literal (x={"constructor":1}) is a String node whose parent is Property → denylist-checked. The same quoted key in a destructuring pattern (const {"constructor":F}=Array) is a String node whose parent is PatternPropertynever checked (verified locally with @lezer/javascript: parent PatternProperty vs Property).
  2. The runtime guard ____safe____property(...) is injected only by processBracketLeft/processBracketRight around [/] tokens (isPropertyNameInBracket covers MemberExpression, Property, PatternProperty — but destructuring has no bracket tokens), so no runtime coercion/denylist happens either.
  3. Array is an explicitly allowed global (authorizeGlobals), so Array → destructure "constructor"FunctionF("return process.getBuiltinModule('child_process').execSync(...)")() executes OS commands as the platform process user (root in the official image).

Sink chain: POST /graphqljsonMapperTest(configuration, file) (@auth(for: [CSVMAPPERS])) → jsonMapper-domain.ts::jsonMapperTestparser/json-mapper.ts::jsonMappingExecutionextractComplexPathFromJsonsafeRender("<?- " + formula + " ?>", ...).

Reproduction Steps

  1. bundle/repro/reproduction_steps.sh (self-contained; only needs Docker).
  2. The script deploys the real stack — elasticsearch:8.19.9, redis:8.4.0, rabbitmq:4.2.2-management, minio/minio:RELEASE.2025-06-13T11-33-47Z, and opencti/platform:6.9.5 (digest pinned and verified: sha256:1f91ad32f1aadf283b5f369ff7b358da071679d4e2bec64030127306db8e73b0) — waits for the real /health endpoint, then:
    • proves CVE-2026-27960 auth bypass remotely (me query as admin via Bearer admin internal_id),
    • sends the negative control: the original CVE-2026-39980 computed-key payload, which the 6.9.5 fix rejects with VerifierIllegalAccessError: Forbidden property access {"propertyName":"constructor"} (proving the fix was active),
    • sends the destructuring exploit formula as a JsonMapper variable formula through jsonMapperTest and verifies a unique per-run marker file inside the platform container containing the token, uid=0(root), and the container hostname,
    • recreates the platform container (fresh process, distinct hostname) and repeats the exploit successfully.
  3. Expected evidence: marker files with uid=0(root) + per-run epoch token + container hostname; GraphQL 200 responses; negative-control rejection; exit code 0.
  4. bundle/repro/negative_control.sh runs a dedicated negative control on a separate fresh process (distinct marker name) and asserts rejection + marker absence.

Evidence

  • bundle/logs/reproduction_steps.log — full pass transcript (two consecutive passes, both exit 0).
  • bundle/artifacts/http/me_response.json{"data":{"me":{"user_email":"admin@pruva.local","name":"admin"}}} via Bearer UUID only (CVE-2026-27960).
  • bundle/artifacts/http/negative_control_response.json — rejection of the original payload; the error message even shows the injected guard: <?- ({}[____safe____property({toString:()=>"constructor"})]...) ... Forbidden property access {"propertyName":"constructor"}.
  • bundle/artifacts/http/exploit_attempt{1,2}_response.json — HTTP 200 {"data":{"jsonMapperTest":{...}}} for the destructuring payload (request bodies preserved in *.operations.json).
  • bundle/artifacts/markers/marker_attempt1.txt (pass 2, container f7cd127ef744), marker_attempt2.txt (pass 2, fresh container e8d8ea37e8dd): per-run token + uid=0(root) gid=0(root)... + hostname. Pass 1 used containers fc0ed8fda7f3 / ee1736911ce1 — four distinct fresh processes total.
  • bundle/repro/runtime_manifest.json — endpoint/runtime evidence manifest with pinned target identity.
  • Environment: rootless Docker 27.5.1, x86_64 Linux, Node runtime bundled in the image.

Recommendations / Next Steps

  • Denylist quoted destructuring keys: in processString, also handle parentType === 'PatternProperty' (and consider PropertyDefinition/assignment patterns), or better, switch from a denylist to an allowlist of permitted property names.
  • Treat constructor-family access uniformly regardless of syntax surface (dot, bracket, destructuring, default values, rest patterns).
  • Isolate formula/template evaluation from the main Node.js process (worker with restricted process/module access); note safeEjs.client.ts already has a worker path — the jsonMapper path uses the in-process safeEjs.ts.
  • Upgrade guidance: 6.9.5 is not sufficient remediation for CVE-2026-39980. CVE-2026-27960 (auth bypass) is fixed in 6.9.13; upgrading to ≥6.9.13 removes the pre-auth vector but the destructuring sandbox escape should be verified/fix-forwarded independently.
  • Testing: add AST-level regression tests feeding {"constructor":...} destructuring patterns into the verifier, plus end-to-end jsonMapperTest exploit tests.

Additional Notes

  • Idempotency: the script tears down and recreates the full stack on every run (docker rm -f + fresh network), generates a fresh epoch token and fresh admin API token per run, and cleans up via trap ... EXIT. Verified idempotent: two consecutive runs both exited 0.
  • Limitations: the exploit requires the formula to avoid bracket property access on forbidden names and this/import; the demonstrated formula is minimal and stable. The Docker-based stack requires ~2 GB RAM for the ES heap; timings on this host: full pass ≈ 4–5 minutes.
  • The Unicode-escape vector hypothesized in public write-ups was ruled out by the discovering run (escaped identifiers throw VerifierParsingError); the residual bypass class is destructuring, confirmed here at runtime.

CVE-2026-39980 Reproduction Transcript

The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.

Full session Replay every step — scrub the timeline or play it back.

Event 1/40
0:005:20
0:00
session startedaccounts/fireworks/models/kimi-k3 · CVE-2026-39980 · REPRO-20
1:56
1:56
extract_facts
no facts extracted
2:48
2:48
supportclaim_contract
3:03
3:03
3:03
3:14
3:14
3:14
3:22
3:22
3:23
3:40
3:40
3:41
$python3 -c " import json d=json.load(open('bundle/learning/exploit_knowledge_preflight.json')) for r in d['results']: rec=r['record'] print('ID:',rec.get('record_id') or rec.get('id')) body=rec.get('body',{}) print(' type:',body.get('record_type'),'title:',rec.get('title')) print(' summary:',(rec.get('summary') or '')[:300]) print('---') "
0.3s
EXPLOIT-KNOWLEDGE PREFLIGHT REVIEW REQUIRED: `run_shell` is blocked until this agent calls `review_exploit_knowledge_preflight` with snapshot_sha256 `c44b863005fab787c1702a1b818e1039319cdf49b2a1e64cf714fc44d9d481f9` and exactly one disposition for each candidate ID [2bbf1277-7e0a-40ad-bbc7-dcc2cd1b07a1, 51e25bd6-5f60-45cd-9422-4eb98bcb8cd6, 9c599f34-064b-4487-839a-83d04c6a0d5f, becefd37-8130-4f46-bf8e-e16efa26d139, e5629ac2-8476-4835-8575-b74310392f84, f2d5c8c1-6e07-4040-945f-5eba4aac3963]. Hist...
3:41
error

Unknown error

4:06
4:06
$true
0.4s
EXPLOIT-KNOWLEDGE PREFLIGHT REVIEW REQUIRED: `run_shell` is blocked until this agent calls `review_exploit_knowledge_preflight` with snapshot_sha256 `c44b863005fab787c1702a1b818e1039319cdf49b2a1e64cf714fc44d9d481f9` and exactly one disposition for each candidate ID [2bbf1277-7e0a-40ad-bbc7-dcc2cd1b07a1, 51e25bd6-5f60-45cd-9422-4eb98bcb8cd6, 9c599f34-064b-4487-839a-83d04c6a0d5f, becefd37-8130-4f46-bf8e-e16efa26d139, e5629ac2-8476-4835-8575-b74310392f84, f2d5c8c1-6e07-4040-945f-5eba4aac3963]. Hist...
4:06
error

Unknown error

4:29
4:52
4:52
4:52
error

Unknown error

5:02
5:20
5:20
08 · How to Fix

How to Fix CVE-2026-39980

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-39980 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-39980

Is CVE-2026-39980 exploitable?

Yes. Pruva independently reproduced CVE-2026-39980 in opencti-platform/opencti and verified the exploit fires end-to-end in a sandboxed environment. A runnable proof-of-concept script and the full agent transcript are on this page (reproduction REPRO-2026-00331).

How severe is CVE-2026-39980?

CVE-2026-39980 is rated critical severity.

What type of vulnerability is CVE-2026-39980?

CVE-2026-39980 is classified as CWE-94 (Improper Control of Generation of Code ('Code Injection')), a RCE vulnerability.

How can I reproduce CVE-2026-39980?

Pruva provides a verified reproduction script on this page. Download it and run it inside an isolated environment such as a container or virtual machine — never against production. The reproduction was confirmed end-to-end by Pruva's automated agents.

Is the CVE-2026-39980 reproduction verified?

Yes. Pruva reproduced CVE-2026-39980 with high confidence in a sandboxed environment, capturing the full agent transcript and artifacts as evidence.
11 · References

References for CVE-2026-39980

Authoritative sources for CVE-2026-39980 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.