- A path traversal in FortiSandbox's JRPC API (CVSS 9.1) lets unauthenticated attackers read system info, scan configs, and download a 32 KB encrypted backup with a single ../../tmp/ payload.
- Bytecode-level look at why /tmp/ is the perfect bypass target, what the patch is, and how it chains with CVE-2026-39808 for full root RCE.
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_keyvia 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 3600sThat's the entire check. Three inputs, three results:
| Input | Resolved path | Result |
|---|---|---|
"abc123def456..." (32-hex) | /usr/rpcsess/abc123def456... | Normal auth |
"../../tmp/" | /usr/rpcsess/../../tmp/ → /tmp/ | Bypass |
"../../etc/hostname" | /usr/rpcsess/../../etc/hostname | Bypass |
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_keyvia 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
| Property | CVE-2026-39813 | CVE-2026-39808 |
|---|---|---|
| Type | Path traversal / auth bypass | OS command injection |
| CWE | CWE-24 | CWE-78 |
| CVSS | 9.1 | 9.1 |
| Affected | 4.4.0–4.4.8 + 5.0.0–5.0.5 | 4.4.0–4.4.8 only |
| Native impact | Read-only data disclosure | Root RCE |
| Trigger | Single HTTP request | Requires triggering a scan |
| Reporter | Loic 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:
- 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. - 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.
- 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 repeatedsys/statusorbackup/configcalls 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.
