@@ -13,15 +13,21 @@ const tmpdir = require('../common/tmpdir');
1313tmpdir . refresh ( ) ;
1414const allowedDir = tmpdir . path ;
1515const allowedFile = path . join ( allowedDir , 'ok.txt' ) ;
16- const subFile = path . join ( allowedDir , 'sub' , 'nested.txt' ) ;
17- fs . mkdirSync ( path . dirname ( subFile ) , { recursive : true } ) ;
16+ const subDir = path . join ( allowedDir , 'sub' ) ;
17+ const subFile = path . join ( subDir , 'nested.txt' ) ;
18+ fs . mkdirSync ( subDir , { recursive : true } ) ;
1819const deniedDir = fs . mkdtempSync ( path . join ( path . dirname ( tmpdir . path ) , 'perm-deny-' ) ) ;
1920const deniedFile = path . join ( deniedDir , 'secret.txt' ) ;
2021fs . writeFileSync ( allowedFile , 'allowed\n' ) ;
2122fs . writeFileSync ( subFile , 'nested\n' ) ;
2223fs . writeFileSync ( deniedFile , 'secret\n' ) ;
2324
24- function runWorkerCase ( workerBody , execArgvFragment ) {
25+ // Trailing-slash allow path (NormalizePathForCompare)
26+ const allowedDirSlash = allowedDir . endsWith ( path . sep )
27+ ? allowedDir
28+ : allowedDir + path . sep ;
29+
30+ function runWithParent ( parentArgs , workerBody , execArgvFragment ) {
2531 const code = `
2632 const { Worker } = require('worker_threads');
2733 const w = new Worker(${ JSON . stringify ( workerBody ) } , {
@@ -36,14 +42,23 @@ function runWorkerCase(workerBody, execArgvFragment) {
3642 ` ;
3743 return spawnSync (
3844 process . execPath ,
39- [ '--permission' , `--allow-fs-read= ${ allowedDir } ` , '--allow-worker' , '- e', code ] ,
45+ parentArgs . concat ( [ '-e' , code ] ) ,
4046 { encoding : 'utf8' , timeout : 30000 , env : { ...process . env } } ,
4147 ) ;
4248}
4349
50+ function baseParent ( extra = [ ] ) {
51+ return [
52+ '--permission' ,
53+ `--allow-fs-read=${ allowedDir } ` ,
54+ '--allow-worker' ,
55+ ...extra ,
56+ ] ;
57+ }
58+
4459function parseLastJsonLine ( stdout ) {
4560 const lines = stdout . trim ( ) . split ( '\n' ) . filter ( Boolean ) ;
46- assert . ok ( lines . length > 0 , ' expected worker JSON output' ) ;
61+ assert . ok ( lines . length > 0 , ` expected JSON, got: ${ stdout } ` ) ;
4762 return JSON . parse ( lines [ lines . length - 1 ] ) ;
4863}
4964
@@ -62,6 +77,19 @@ function workerRead(filePath) {
6277 ` ;
6378}
6479
80+ function workerWrite ( filePath ) {
81+ return `
82+ const { parentPort } = require('worker_threads');
83+ const fs = require('fs');
84+ try {
85+ fs.writeFileSync(${ JSON . stringify ( filePath ) } , 'w');
86+ parentPort.postMessage({ ok: true });
87+ } catch (err) {
88+ parentPort.postMessage({ ok: false, code: err.code });
89+ }
90+ ` ;
91+ }
92+
6593function assertAccessDenied ( msg ) {
6694 assert . strictEqual ( msg . ok , false , JSON . stringify ( msg ) ) ;
6795 assert . strictEqual ( msg . code , 'ERR_ACCESS_DENIED' ) ;
@@ -72,63 +100,189 @@ function assertOk(msg) {
72100}
73101
74102try {
103+ // default Worker inherits
75104 {
76- const r = runWorkerCase ( workerRead ( deniedFile ) , '' ) ;
105+ const r = runWithParent ( baseParent ( ) , workerRead ( deniedFile ) , '' ) ;
77106 assert . strictEqual ( r . status , 0 , r . stderr ) ;
78107 assertAccessDenied ( parseLastJsonLine ( r . stdout ) ) ;
79108 }
109+
110+ // execArgv: []
80111 {
81- const denied = runWorkerCase ( workerRead ( deniedFile ) , 'execArgv: [],' ) ;
112+ const denied = runWithParent ( baseParent ( ) , workerRead ( deniedFile ) , 'execArgv: [],' ) ;
82113 assert . strictEqual ( denied . status , 0 , denied . stderr ) ;
83114 assertAccessDenied ( parseLastJsonLine ( denied . stdout ) ) ;
84- const allowed = runWorkerCase ( workerRead ( allowedFile ) , 'execArgv: [],' ) ;
115+ const allowed = runWithParent ( baseParent ( ) , workerRead ( allowedFile ) , 'execArgv: [],' ) ;
85116 assert . strictEqual ( allowed . status , 0 , allowed . stderr ) ;
86117 assertOk ( parseLastJsonLine ( allowed . stdout ) ) ;
87- const nested = runWorkerCase ( workerRead ( subFile ) , 'execArgv: [],' ) ;
118+ const nested = runWithParent ( baseParent ( ) , workerRead ( subFile ) , 'execArgv: [],' ) ;
88119 assert . strictEqual ( nested . status , 0 , nested . stderr ) ;
89120 assertOk ( parseLastJsonLine ( nested . stdout ) ) ;
90121 }
122+
123+ // non-permission flag preserved + ceiling
91124 {
92- const denied = runWorkerCase ( workerRead ( deniedFile ) , 'execArgv: ["--no-warnings"],' ) ;
125+ const denied = runWithParent (
126+ baseParent ( ) ,
127+ workerRead ( deniedFile ) ,
128+ 'execArgv: ["--no-warnings"],' ,
129+ ) ;
93130 assert . strictEqual ( denied . status , 0 , denied . stderr ) ;
94131 assertAccessDenied ( parseLastJsonLine ( denied . stdout ) ) ;
95- const allowed = runWorkerCase ( workerRead ( allowedFile ) , 'execArgv: ["--no-warnings"],' ) ;
132+ const allowed = runWithParent (
133+ baseParent ( ) ,
134+ workerRead ( allowedFile ) ,
135+ 'execArgv: ["--no-warnings"],' ,
136+ ) ;
96137 assert . strictEqual ( allowed . status , 0 , allowed . stderr ) ;
97138 assertOk ( parseLastJsonLine ( allowed . stdout ) ) ;
98139 }
140+
141+ // worker permission on, no fs-read → empty grants
99142 {
100143 const frag = `execArgv: ${ JSON . stringify ( [ '--permission' , '--allow-worker' ] ) } ,` ;
101- const d = runWorkerCase ( workerRead ( deniedFile ) , frag ) ;
144+ const d = runWithParent ( baseParent ( ) , workerRead ( deniedFile ) , frag ) ;
102145 assert . strictEqual ( d . status , 0 , d . stderr ) ;
103146 assertAccessDenied ( parseLastJsonLine ( d . stdout ) ) ;
104- const a = runWorkerCase ( workerRead ( allowedFile ) , frag ) ;
147+ const a = runWithParent ( baseParent ( ) , workerRead ( allowedFile ) , frag ) ;
105148 assert . strictEqual ( a . status , 0 , a . stderr ) ;
106149 assertAccessDenied ( parseLastJsonLine ( a . stdout ) ) ;
107150 }
151+
152+ // escalate * clamped
108153 {
109- const frag = `execArgv: ${ JSON . stringify ( [ '--permission' , '--allow-fs-read=*' , '--allow-worker' ] ) } ,` ;
110- const r = runWorkerCase ( workerRead ( deniedFile ) , frag ) ;
154+ const frag = `execArgv: ${ JSON . stringify ( [
155+ '--permission' , '--allow-fs-read=*' , '--allow-worker' ,
156+ ] ) } ,`;
157+ const r = runWithParent ( baseParent ( ) , workerRead ( deniedFile ) , frag ) ;
111158 assert . strictEqual ( r . status , 0 , r . stderr ) ;
112159 assertAccessDenied ( parseLastJsonLine ( r . stdout ) ) ;
113160 }
161+
162+ // repeated allow flags cannot exceed parent
114163 {
115164 const frag = `execArgv: ${ JSON . stringify ( [
116165 '--permission' ,
117166 `--allow-fs-read=${ allowedDir } ` ,
118167 `--allow-fs-read=${ deniedFile } ` ,
119168 '--allow-worker' ,
120169 ] ) } ,`;
121- const r = runWorkerCase ( workerRead ( deniedFile ) , frag ) ;
170+ const r = runWithParent ( baseParent ( ) , workerRead ( deniedFile ) , frag ) ;
122171 assert . strictEqual ( r . status , 0 , r . stderr ) ;
123172 assertAccessDenied ( parseLastJsonLine ( r . stdout ) ) ;
124173 }
174+
175+ // subset path under parent dir
125176 {
126177 const frag = `execArgv: ${ JSON . stringify ( [
127178 '--permission' ,
128179 `--allow-fs-read=${ subFile } ` ,
129180 '--allow-worker' ,
130181 ] ) } ,`;
131- const r = runWorkerCase ( workerRead ( subFile ) , frag ) ;
182+ const r = runWithParent ( baseParent ( ) , workerRead ( subFile ) , frag ) ;
183+ assert . strictEqual ( r . status , 0 , r . stderr ) ;
184+ assertOk ( parseLastJsonLine ( r . stdout ) ) ;
185+ }
186+
187+ // trailing slash parent allow (NormalizePathForCompare)
188+ {
189+ const parent = [
190+ '--permission' ,
191+ `--allow-fs-read=${ allowedDirSlash } ` ,
192+ '--allow-worker' ,
193+ ] ;
194+ const r = runWithParent ( parent , workerRead ( allowedFile ) , 'execArgv: [],' ) ;
195+ assert . strictEqual ( r . status , 0 , r . stderr ) ;
196+ assertOk ( parseLastJsonLine ( r . stdout ) ) ;
197+ }
198+
199+ // parent has wildcard * → worker * / paths not stripped by Filter
200+ {
201+ const parent = [ '--permission' , '--allow-fs-read=*' , '--allow-worker' ] ;
202+ const frag = `execArgv: ${ JSON . stringify ( [
203+ '--permission' ,
204+ `--allow-fs-read=${ deniedFile } ` ,
205+ '--allow-worker' ,
206+ ] ) } ,`;
207+ const r = runWithParent ( parent , workerRead ( deniedFile ) , frag ) ;
208+ assert . strictEqual ( r . status , 0 , r . stderr ) ;
209+ assertOk ( parseLastJsonLine ( r . stdout ) ) ;
210+ }
211+
212+ // permission-audit on parent + empty execArgv
213+ {
214+ const parent = [
215+ '--permission' ,
216+ '--permission-audit' ,
217+ `--allow-fs-read=${ allowedDir } ` ,
218+ '--allow-worker' ,
219+ ] ;
220+ const r = runWithParent ( parent , workerRead ( allowedFile ) , 'execArgv: [],' ) ;
221+ assert . strictEqual ( r . status , 0 , r . stderr ) ;
222+ assertOk ( parseLastJsonLine ( r . stdout ) ) ;
223+ }
224+
225+ // fs-write: parent no write → worker cannot escalate write
226+ {
227+ const writeTarget = path . join ( allowedDir , 'w.txt' ) ;
228+ const frag = `execArgv: ${ JSON . stringify ( [
229+ '--permission' ,
230+ `--allow-fs-write=${ allowedDir } ` ,
231+ '--allow-worker' ,
232+ ] ) } ,`;
233+ const r = runWithParent ( baseParent ( ) , workerWrite ( writeTarget ) , frag ) ;
234+ assert . strictEqual ( r . status , 0 , r . stderr ) ;
235+ assertAccessDenied ( parseLastJsonLine ( r . stdout ) ) ;
236+ }
237+
238+ // fs-write allowed when parent grants write
239+ {
240+ const writeTarget = path . join ( allowedDir , 'w2.txt' ) ;
241+ const parent = [
242+ '--permission' ,
243+ `--allow-fs-read=${ allowedDir } ` ,
244+ `--allow-fs-write=${ allowedDir } ` ,
245+ '--allow-worker' ,
246+ ] ;
247+ const frag = `execArgv: ${ JSON . stringify ( [
248+ '--permission' ,
249+ `--allow-fs-write=${ allowedDir } ` ,
250+ '--allow-worker' ,
251+ ] ) } ,`;
252+ const r = runWithParent ( parent , workerWrite ( writeTarget ) , frag ) ;
253+ assert . strictEqual ( r . status , 0 , r . stderr ) ;
254+ assertOk ( parseLastJsonLine ( r . stdout ) ) ;
255+ }
256+
257+ // boolean allow intersection: worker asks allow-net, parent does not
258+ {
259+ const frag = `execArgv: ${ JSON . stringify ( [
260+ '--permission' ,
261+ `--allow-fs-read=${ allowedDir } ` ,
262+ '--allow-net' ,
263+ '--allow-worker' ,
264+ ] ) } ,`;
265+ // still cannot read denied; net flag just exercises Intersect path
266+ const r = runWithParent ( baseParent ( ) , workerRead ( deniedFile ) , frag ) ;
267+ assert . strictEqual ( r . status , 0 , r . stderr ) ;
268+ assertAccessDenied ( parseLastJsonLine ( r . stdout ) ) ;
269+ }
270+
271+ // multiple boolean flags in rebuild path
272+ {
273+ const parent = [
274+ '--permission' ,
275+ `--allow-fs-read=${ allowedDir } ` ,
276+ '--allow-worker' ,
277+ '--allow-addons' ,
278+ ] ;
279+ const frag = `execArgv: ${ JSON . stringify ( [
280+ '--permission' ,
281+ `--allow-fs-read=${ allowedDir } ` ,
282+ '--allow-worker' ,
283+ '--allow-addons' ,
284+ ] ) } ,`;
285+ const r = runWithParent ( parent , workerRead ( allowedFile ) , frag ) ;
132286 assert . strictEqual ( r . status , 0 , r . stderr ) ;
133287 assertOk ( parseLastJsonLine ( r . stdout ) ) ;
134288 }
0 commit comments