Skip to content

Commit d53d582

Browse files
edsadraduh95
authored andcommitted
src,permission: do not throw on denied access in audit mode
The THROW_IF_INSUFFICIENT_PERMISSIONS and ASYNC_THROW_IF_INSUFFICIENT_PERMISSIONS macros called ThrowAccessDenied/AsyncThrowAccessDenied unconditionally and only guarded the `return` with `warning_only()`. ERR_ACCESS_DENIED_IF_INSUFFICIENT_PERMISSIONS had no `warning_only()` guard at all — it always set the access-denied error and returned. As a result, running with `--permission-audit` still produced ERR_ACCESS_DENIED on any denied operation (fs, net, child_process, worker, addon, ffi, inspector, wasi), defeating the audit-only purpose of the flag. Guard the denied-error path behind `!warning_only()` in all three macros. In audit mode, the diagnostics-channel message is published (already done in Permission::is_scope_granted) and execution continues; in enforce mode (`--permission`), behavior is unchanged — the error is raised and the call returns. The tests cover both the direct (top-level) call and an `eval()`-wrapped call: the direct call exercises the normal script path, and the `eval()`-wrapped call exercises the V8 script-context boundary (the diagnostics subscriber is registered in the outer module context while the denied operation runs inside an eval'd string). Refs: 9ddd1a9 Signed-off-by: Adrian Estrada <edsadr@gmail.com> PR-URL: #64426 Backport-PR-URL: #65354 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent c40d147 commit d53d582

2 files changed

Lines changed: 79 additions & 13 deletions

File tree

src/permission/permission.h

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ namespace permission {
3535
const auto resource__ = (resource); \
3636
if (!env__->permission()->is_granted(env__, perm__, resource__)) \
3737
[[unlikely]] { \
38-
node::permission::Permission::ThrowAccessDenied( \
39-
env__, perm__, resource__); \
40-
if (!env__->permission()->warning_only()) return __VA_ARGS__; \
38+
if (!env__->permission()->warning_only()) { \
39+
node::permission::Permission::ThrowAccessDenied( \
40+
env__, perm__, resource__); \
41+
return __VA_ARGS__; \
42+
} \
4143
} \
4244
} while (0)
4345

@@ -49,9 +51,11 @@ namespace permission {
4951
const auto resource__ = (resource); \
5052
if (!env__->permission()->is_granted(env__, perm__, resource__)) \
5153
[[unlikely]] { \
52-
node::permission::Permission::AsyncThrowAccessDenied( \
53-
env__, (wrap), perm__, resource__); \
54-
if (!env__->permission()->warning_only()) return __VA_ARGS__; \
54+
if (!env__->permission()->warning_only()) { \
55+
node::permission::Permission::AsyncThrowAccessDenied( \
56+
env__, (wrap), perm__, resource__); \
57+
return __VA_ARGS__; \
58+
} \
5559
} \
5660
} while (0)
5761

@@ -63,14 +67,17 @@ namespace permission {
6367
const auto resource__ = (resource); \
6468
if (!env__->permission()->is_granted(env__, perm__, resource__)) \
6569
[[unlikely]] { \
66-
Local<Value> err_access; \
67-
if (node::permission::CreateAccessDeniedError(env__, perm__, resource__) \
68-
.ToLocal(&err_access)) { \
69-
args.GetReturnValue().Set(err_access); \
70-
} else { \
71-
args.GetReturnValue().Set(UV_EACCES); \
70+
if (!env__->permission()->warning_only()) { \
71+
Local<Value> err_access; \
72+
if (node::permission::CreateAccessDeniedError( \
73+
env__, perm__, resource__) \
74+
.ToLocal(&err_access)) { \
75+
args.GetReturnValue().Set(err_access); \
76+
} else { \
77+
args.GetReturnValue().Set(UV_EACCES); \
78+
} \
79+
return __VA_ARGS__; \
7280
} \
73-
return __VA_ARGS__; \
7481
} \
7582
} while (0)
7683

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const { isMainThread } = require('worker_threads');
5+
6+
if (!isMainThread) {
7+
common.skip('This test only works on a main thread');
8+
}
9+
10+
const assert = require('assert');
11+
const { spawnSync } = require('child_process');
12+
const { test } = require('node:test');
13+
const fixtures = require('../common/fixtures');
14+
15+
const blockedFile = fixtures.path('permission', 'deny', 'protected-file.md');
16+
17+
function runAudit(mode) {
18+
const childScript = `
19+
const dc = require('node:diagnostics_channel');
20+
const msgs = [];
21+
dc.subscribe('node:permission-model:fs', (m) => msgs.push({
22+
permission: m.permission,
23+
resource: m.resource,
24+
}));
25+
try {
26+
${mode === 'eval' ?
27+
`eval('require("node:fs").readFileSync(process.env.BLOCKED_FILE)');` :
28+
`require('node:fs').readFileSync(process.env.BLOCKED_FILE);`}
29+
console.log('RESULT NO_THROW');
30+
} catch (e) {
31+
console.log('RESULT THREW ' + e.code);
32+
}
33+
console.log('AUDIT ' + JSON.stringify(msgs));
34+
`;
35+
36+
const env = { ...process.env, BLOCKED_FILE: blockedFile };
37+
const { status, stdout, stderr } = spawnSync(
38+
process.execPath,
39+
['--permission-audit', '-e', childScript],
40+
{ encoding: 'utf8', env },
41+
);
42+
assert.strictEqual(status, 0, stderr);
43+
const lines = stdout.split('\n');
44+
assert.ok(lines.includes('RESULT NO_THROW'), stdout);
45+
const auditLine = lines.find((l) => l.startsWith('AUDIT '));
46+
assert.ok(auditLine, stdout);
47+
const msgs = JSON.parse(auditLine.replace('AUDIT ', ''));
48+
assert.strictEqual(msgs.length, 1);
49+
assert.strictEqual(msgs[0].permission, 'FileSystemRead');
50+
assert.ok(msgs[0].resource.endsWith('protected-file.md'));
51+
}
52+
53+
test('permission-audit logs fs denial without throwing', () => {
54+
runAudit('direct');
55+
});
56+
57+
test('permission-audit logs fs denial without throwing (eval)', () => {
58+
runAudit('eval');
59+
});

0 commit comments

Comments
 (0)