TL;DR

CVE-2026-39813 (CVSS 9.1, CWE-24) is a path traversal in the is_valid_session() function of the FortiSandbox JRPC API. Attacker-controlled session_id is fed straight into os.path.join() with zero validation. Send session = "../../tmp/" in a JSON-RPC body and the system mistakes /tmp/ for a valid session directory — bypass complete. No credentials, no clicks, one HTTP request.

Affected: FortiSandbox 4.4.0–4.4.8 and 5.0.0–5.0.5. Fixed: 4.4.9 and 5.0.6. Versions 4.2 and 5.2 are not impacted. Disclosed 2026-04-14 by Loic Pantano of Fortinet PSIRT.

What's new

On 2026-04-14 Fortinet published advisory FG-IR-26-112 alongside FG-IR-26-100 covering CVE-2026-39808 (OS command injection). Both are unauthenticated, both score CVSS 9.1, and crucially — they chain. The path traversal alone leaks data; combined with the command injection it yields root RCE on the very appliance enterprises trust to detonate malware.

Six days later the imjdl blog published a bytecode-level write-up reverse-engineering the firmware, reconstructing the full exploit chain, and confirming the patch. That post is the source of most technical detail below.

Why it matters

FortiSandbox isn't a leaf node — it's the security oracle for the Fortinet Security Fabric. Firewalls, FortiMail, FortiEDR, SIEM and SOAR consume its verdicts to enforce blocking and trigger playbooks. If an attacker silently owns the sandbox, they can:

  • Mark malicious files as clean, silently disabling network-wide blocking.
  • Pivot from a high-trust internal appliance to anywhere in the fabric.
  • Read the encrypted system backup, then steal the encryption key from /etc/secret_key via the same traversal — offline decryption gives admin hashes, scan rules, integration secrets.

Worst part: the bug needs zero prerequisites. A single HTTP request from the open internet (or the corporate LAN) is enough.

Technical facts

The vulnerable code lives in apps/jsonrpc/rpcsession.pyc. The is_valid_session() function takes session_id from the JSON request body and concatenates it with the session directory:

fpath = os.path.join(DIRRPCSESS, session_id)
# "valid" if path exists AND mtime within 3600s

That's the entire check. Three inputs, three results:

InputResolved pathResult
"abc123def456..." (32-hex)/usr/rpcsess/abc123def456...Normal auth
"../../tmp/"/usr/rpcsess/../../tmp//tmp/Bypass
"../../etc/hostname"/usr/rpcsess/../../etc/hostnameBypass

Why /tmp/ wins: every FortiSandbox has it, and cron jobs, log rotation and Redis caches constantly write to it — the mtime check passes every time. Even better, the legitimate renew_session() path calls os.path.utime(fpath, None) after success, so once you're in, your forged session stays valid forever as long as you ping the API at least once an hour.

Once authenticated, the attacker can:

  • Read 26 fields of system info from sys/status (version, hostname, serial, CPU/RAM/disk, scan config).
  • Enumerate 50+ JRPC endpoints; read-only ones are fully exploitable.
  • Download a 32 KB encrypted backup from backup/config.
  • Pull /etc/secret_key via a second traversal and decrypt the backup offline.

Bonus design flaw exposed in the same audit: rpc.pyc::process() has a second handler path triggered by Content-Type: application/json that completely skips the session check. Defense-in-depth becomes defense-in-luck.

The CVSS 3.1 vector (per Fortinet's CNA entry on NVD): AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H — network attack, low complexity, no privileges, no interaction, all three impacts high. Fortinet rates it 9.1; NVD CNA shows 9.8.

Comparison & chaining with CVE-2026-39808

PropertyCVE-2026-39813CVE-2026-39808
TypePath traversal / auth bypassOS command injection
CWECWE-24CWE-78
CVSS9.19.1
Affected4.4.0–4.4.8 + 5.0.0–5.0.54.4.0–4.4.8 only
Native impactRead-only data disclosureRoot RCE
TriggerSingle HTTP requestRequires triggering a scan
ReporterLoic Pantano (Fortinet PSIRT)Samuel de Lucas Maroto (KPMG Spain)

Alone, 39813 is “just” data leakage. Chained with 39808, an attacker walks from anonymous HTTP to root shell on the sandbox — and from there into the rest of the Security Fabric.

Use cases & lateral movement

Three realistic attack patterns to expect:

  1. Reconnaissance at scale. Internet scanners spray the ../../tmp/ payload, fingerprint serial numbers and firmware versions, build a target list of unpatched 4.4.x / 5.0.x boxes.
  2. Silent verdict tampering. Once chained to RCE, the attacker poisons the verdict pipeline so phishing payloads come back “clean” to FortiMail, FortiGate and downstream EDR.
  3. Internal pivot. The sandbox usually sits in a trusted segment with administrative reach into the fabric. Owning it is a credentialed foothold by proxy.

Limitations & mitigations

The bug isn't omnipotent on its own. Write-permission endpoints (password change, network config) reject the traversal because get_username_by_sessionid() tries open("/tmp/", "r")IsADirectoryError → INVALID_SESSION. So no native admin takeover from 39813 alone — you need 39808 (or another bug) for code execution.

No exploitation in the wild has been observed at the time Fortinet, Field Effect and Help Net Security published. But the imjdl deep-dive is essentially a working PoC, so opportunistic scanning is the obvious next step.

Action checklist:

  • Upgrade FortiSandbox 4.4.x → 4.4.9; 5.0.x → 5.0.6. PaaS deployments unaffected.
  • If you can't patch immediately: WAF rule blocking JRPC bodies containing ../; restrict /jsonrpc/ to trusted IPs only.
  • Hunt for anomalous /jsonrpc/ traffic, especially repeated sys/status or backup/config calls from non-administrative sources.

The fix itself is one regex: re.match(r'^[A-Za-z0-9]{32}$', session_id) applied before os.path.join. Legitimate IDs from secrets.token_hex(16) always match; ../../tmp/ never does. Boring, correct, ship it.

What's next

Public exploit code hasn't appeared yet, but the bytecode walkthrough leaves nothing to imagine. Within days expect Shodan-style scans hunting exposed JRPC interfaces. Defenders should prioritise inventory — many FortiSandbox boxes live in DMZs or on management subnets that feel internal but aren't.

Sources: Fortinet PSIRT FG-IR-26-112, NVD, imjdl deep dive, Help Net Security, Field Effect.