# Deno: Permission Bypass via Unicode Normalization Mismatch on macOS (APFS)

Deno's macOS deny-rule path check compared raw bytes while APFS resolves Unicode/case-equivalent names to the same inode, letting sandboxed code read, write, run or FFI-load denied paths.

- Payout: Research
- Severity: high
- Program: Deno
- Found by: tomasilluminati
- Disclosed: 2026-06-16
- CVE: CVE-2026-49401
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-8xpq-cjcf-3wh9
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-8xpq-cjcf-3wh9
- Tags: area:os domain:desktop impact:integrity impact:privesc kind:research platform:ghsa sev:high tech:rust technique:unicode-normalization vuln:access-control vuln:path-traversal vuln:sandbox-escape

**Target:** Deno permission system path comparison (--deny-read/write/run/ffi) on macOS APFS
**Root cause:** Deny-rule paths and requested paths were compared at the raw-byte level, but APFS treats NFC/NFD spellings, case variants, ligatures and ß/ss as the same file, so byte-different paths passed the check and the kernel resolved them to the same inode.
**Steps:**
1. Run Deno on macOS with a broad allow plus a deny carve-out, e.g. `--allow-read --deny-read=/secrets/passwörter.txt`
2. From the sandboxed script, open the denied file using an NFD spelling: `/secrets/passwo\u0308rter.txt`
3. Alternatively use a case variant `/SECRETS/PASSWÖRTER.txt` (default APFS is case-insensitive) or ligature/ß equivalents (`ﬁ` vs `fi`, `ß` vs `ss`)
4. Deno's byte-level comparison does not match the deny rule, the check passes, and the kernel serves the same inode
5. Repeat against `--deny-write`, `--deny-run`, `--deny-ffi` which share the comparison code
**How found:** Reporter noticed Deno's permission check operated on raw bytes while the underlying macOS APFS filesystem applies Unicode normalization and case folding, hypothesizing that equivalent spellings would diverge between the check and kernel resolution.
**Exploitation:** A dependency, plugin or attacker-controlled input running under broad --allow-* with --deny-* carve-outs references the protected path through a Unicode- or case-equivalent spelling; the permission check passes on bytes while APFS resolves to the denied inode, granting read, write, execution or FFI load of the denied path.
**Escalation:** Same comparison code shared across --deny-read, --deny-write, --deny-run and --deny-ffi widens the bypass from file read to write, process execution and native library loading.
**Insight:** When a sandbox compares paths as strings but the OS resolves them semantically, enumerate the filesystem's equivalence classes (NFC/NFD, case folding, ligatures, ß/ss) and test each against deny rules; denylists are inherently weaker than allowlists here.

