Skip to content

Commit 56063d0

Browse files
committed
feat: US-105 - Add assertPayloadByteLength to text file reads
1 parent 84d0869 commit 56063d0

4 files changed

Lines changed: 58 additions & 2 deletions

File tree

packages/secure-exec-node/src/bridge-setup.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,13 @@ export async function setupRequire(
267267
// Create individual References for each fs operation
268268
const readFileRef = new ivm.Reference(async (path: string) => {
269269
checkBridgeBudget(deps);
270-
return fs.readTextFile(path);
270+
const text = await fs.readTextFile(path);
271+
assertTextPayloadSize(
272+
`fs.readFile ${path}`,
273+
text,
274+
fsJsonPayloadLimit,
275+
);
276+
return text;
271277
});
272278
const writeFileRef = new ivm.Reference(
273279
async (path: string, content: string) => {

packages/secure-exec/tests/runtime-driver/node/payload-limits.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,44 @@ describe("NodeRuntime payload limits", () => {
393393
expect(capture.stdout()).toBe("hello\n");
394394
});
395395

396+
it("rejects oversized text file reads", async () => {
397+
const fs = createInMemoryFileSystem();
398+
const oversizedText = "x".repeat(DEFAULT_ISOLATE_JSON_PAYLOAD_BYTES + 1);
399+
await fs.mkdir("/data");
400+
await fs.writeFile("/data/too-large.txt", oversizedText);
401+
402+
proc = createTestNodeRuntime({ filesystem: fs, permissions: allowAllFs });
403+
const result = await proc.exec(`
404+
const fs = require('fs');
405+
fs.readFileSync('/data/too-large.txt', 'utf8');
406+
`);
407+
408+
expect(result.code).toBe(1);
409+
expect(result.errorMessage).toContain(PAYLOAD_LIMIT_ERROR_CODE);
410+
expect(result.errorMessage).toContain("fs.readFile");
411+
});
412+
413+
it("allows normal-sized text file reads", async () => {
414+
const fs = createInMemoryFileSystem();
415+
await fs.mkdir("/data");
416+
await fs.writeFile("/data/normal.txt", "hello world");
417+
418+
const capture = createConsoleCapture();
419+
proc = createTestNodeRuntime({
420+
filesystem: fs,
421+
permissions: allowAllFs,
422+
onStdio: capture.onStdio,
423+
});
424+
const result = await proc.exec(`
425+
const fs = require('fs');
426+
const content = fs.readFileSync('/data/normal.txt', 'utf8');
427+
console.log(content);
428+
`);
429+
430+
expect(result.code).toBe(0);
431+
expect(capture.stdout()).toBe("hello world\n");
432+
});
433+
396434
it("rejects out-of-range payload limit configuration", () => {
397435
expect(
398436
() => createTestNodeRuntime({ payloadLimits: { jsonPayloadBytes: 0 } }),

scripts/ralph/prd.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2023,7 +2023,7 @@
20232023
"Tests pass"
20242024
],
20252025
"priority": 120,
2026-
"passes": false,
2026+
"passes": true,
20272027
"notes": "Audit C3 — CRITICAL. execution-driver.ts:1030-1032. Binary read path (readFileBinaryRef) correctly calls assertPayloadByteLength, text read path does not. With ModuleAccessFileSystem projecting host node_modules, sandbox code can read arbitrarily large text files into host memory."
20282028
},
20292029
{

scripts/ralph/progress.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1527,3 +1527,15 @@ PRD: ralph/kernel-hardening (46 stories)
15271527
- `deps.processConfig.env` is the init-time filtered env (already filtered by `filterEnv()` in execution-driver.ts) — safe to use as fallback
15281528
- When `options.env` is undefined, `stripDangerousEnv(undefined)` returns undefined — the fallback must happen BEFORE the strip call
15291529
---
1530+
1531+
## 2026-03-18 - US-105
1532+
- What was implemented: Added assertTextPayloadSize guard to readFileRef (text file read bridge path), matching the existing guard in readFileBinaryRef
1533+
- The text read path was missing payload size validation, allowing sandbox code to read arbitrarily large text files into host memory via readFileSync('path', 'utf8')
1534+
- Files changed:
1535+
- packages/secure-exec-node/src/bridge-setup.ts — added assertTextPayloadSize call with fsJsonPayloadLimit before returning text
1536+
- packages/secure-exec/tests/runtime-driver/node/payload-limits.test.ts — added 2 tests: oversized text file read rejection and normal-sized text file read preservation
1537+
- **Learnings for future iterations:**
1538+
- Text file reads use fsJsonPayloadLimit (4MB default) not base64Limit — text is passed directly, not base64-encoded
1539+
- assertTextPayloadSize is the convenience wrapper for text (handles UTF-8 byte length calculation)
1540+
- readFileRef returns string from readTextFile; readFileBinaryRef returns base64-encoded Buffer — different limits and guards needed
1541+
---

0 commit comments

Comments
 (0)