Skip to content

fix(security): RQ-2426 — sandbox rule code (QuickJS-WASM)#112

Open
dinex-dev wants to merge 4 commits into
masterfrom
fix/RQ-2426-sandbox-worker-vm
Open

fix(security): RQ-2426 — sandbox rule code (QuickJS-WASM)#112
dinex-dev wants to merge 4 commits into
masterfrom
fix/RQ-2426-sandbox-worker-vm

Conversation

@dinex-dev

@dinex-dev dinex-dev commented Jun 23, 2026

Copy link
Copy Markdown
Member

What

Sandboxes "Code"-type Modify Request/Response rules. They previously ran rule-supplied JS via new Function(...) directly in the proxy's Node process (full require/process/fs/child_process). Code rules travel between users (shared lists, import/export, team sync) → supply-chain RCE.

Rule code now runs inside QuickJS compiled to WebAssembly (quickjs-emscripten, single-file embedded variant).

⚠️ Branch name fix/RQ-2426-sandbox-worker-vm is legacy — this PR was force-updated from an earlier worker_threads+vm attempt to the QuickJS approach (see "Why" below). Content is QuickJS-WASM.

Why QuickJS-WASM

QuickJS is a separate JS engine in the WASM sandbox — no require/process/fs/global, and no prototype path back to the host (constructor-escape blocked). Only explicitly-injected primitives are reachable. The two alternatives don't work here:

  • isolated-vm — native addon; no build for a currently-supported Electron's V8 (6.x too old for V8 13, 7.x needs Node 26).
  • worker_threads + vmworker_threads cannot create a Worker in an Electron renderer ("The V8 platform … does not support creating Workers"), and the proxy runs in the desktop app's background renderer.

QuickJS-WASM is pure WASM+JS — builds nowhere natively and runs fine in the renderer (verified).

Changes

  • src/utils/index.ts: executeUserFunction runs the rule in QuickJS — 5s deadline interrupt, 128 MB cap. isValidFunctionString compiles via new Function without calling it (parse-only, no execution; no vm). getFunctionFromString removed.
  • Both processors: validate → pass the source string (also fixes the prior request/response call-site inconsistency).
  • Contract preserved: returns a string (objects JSON-stringified), promises awaited, console captured as {type,args}, $sharedState read + written back.

Intentional gap

No fetch/Buffer/timers in rule code. fetch would need the asyncify QuickJS variant + an async host bridge — a follow-up (QuickJS can do it safely, unlike worker+vm).

Verified

  • Sandbox harness 13/13 on Node 24 (incl. a real modify-response rule).
  • Instantiates + runs + blocks host access inside the Electron 42 renderer (typeof process/require = undefined in-sandbox).
  • Before/after exploit probe flips from RCE / file read / env / process accessfully blocked, while a legit rule still works.

Downstream

Desktop needs only a proxy dep bump (folded into #358) — no desktop code change. Ships alongside the Node-24 writeHead fix (#113).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • User-provided code now executes in an isolated sandbox environment with built-in resource limits and automatic validation.
  • Chores

    • Updated project dependencies to include QuickJS sandbox library for enhanced code execution safety.
    • Version updated to 1.4.0.

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@dinex-dev, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 6 minutes and 26 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4fdeb49e-8b00-4b5f-ab6a-99c5ddaa642b

📥 Commits

Reviewing files that changed from the base of the PR and between 2f32940 and b7d6aed.

⛔ Files ignored due to path filters (2)
  • dist/utils/index.js is excluded by !**/dist/**
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (2)
  • package.json
  • src/utils/index.ts

Walkthrough

The PR replaces the previous host-realm new Function approach for executing user rule code with a QuickJS-WASM sandbox. src/utils/index.ts is rewritten to add getQuickJSModule() (singleton), SANDBOX_SETUP (a pre-built sandbox program with console capture and base64 helpers), isValidFunctionString (compile-only validation), and a new executeUserFunction that creates a fresh QuickJS context per invocation, enforces memory/stack/deadline limits, injects JSON-serialized arguments and shared state, and reads back results via a __OUTPUT global. Both modify_request_processor.js and modify_response_processor.js are updated to replace getFunctionFromString with isValidFunctionString for pre-execution validation and to pass raw function strings to executeUserFunction. Two QuickJS packages are added to package.json dependencies.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main security fix: sandboxing rule code execution using QuickJS-WASM to address RQ-2426 RCE vulnerability.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/RQ-2426-sandbox-worker-vm

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Jun 23, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@requestly/requestly-proxy@112

commit: b7d6aed

"Code"-type Modify Request/Response rules ran rule-supplied JS via
`new Function(...)` directly in the proxy's Node process (full require/process/
fs/child_process). Code rules travel between users (shared lists, import/export,
team sync), so this was a supply-chain RCE primitive.

Rule code now runs inside QuickJS compiled to WebAssembly (quickjs-emscripten,
single-file embedded variant). QuickJS is a separate JS engine in the WASM
sandbox: no require/process/fs/global, and no prototype path back to the host
(constructor-escape blocked). Only injected primitives are reachable.

Why QuickJS-WASM (not isolated-vm or worker_threads + vm):
- isolated-vm is a native addon with no build for a supported Electron's V8
  (6.x too old for V8 13, 7.x needs Node 26).
- worker_threads cannot create a Worker in an Electron renderer ("The V8
  platform ... does not support creating Workers"), and the proxy runs in the
  desktop app's background renderer.
QuickJS-WASM is pure WASM+JS — builds nowhere natively and runs in the renderer.

- src/utils/index.ts: executeUserFunction runs in QuickJS; 5s deadline interrupt,
  128MB cap. isValidFunctionString compiles via `new Function` WITHOUT calling it
  (parse-only, no execution). getFunctionFromString removed.
- both Modify Request/Response processors: validate -> pass the source string.
- contract preserved: returns a string (objects JSON-stringified), promises
  awaited, console captured as {type,args}, $sharedState read + written back.
- intentional gap: no fetch/Buffer/timers (fetch needs the asyncify variant +
  async host bridge — a follow-up; QuickJS can do it safely).

Verified: sandbox harness 13/13 (Node 24); instantiates + runs + blocks
host access inside the Electron 42 renderer; before/after exploit probe flips
from RCE/file/env/process access to fully blocked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dinex-dev dinex-dev force-pushed the fix/RQ-2426-sandbox-worker-vm branch from 77c31fd to 2f32940 Compare June 23, 2026 11:23
@dinex-dev dinex-dev changed the title fix(security): RQ-2426 — sandbox rule code (worker_threads + vm) fix(security): RQ-2426 — sandbox rule code (QuickJS-WASM) Jun 23, 2026
@dinex-dev dinex-dev marked this pull request as ready for review June 23, 2026 13:16

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@package.json`:
- Line 3: The version field in package.json has been downgraded from 1.5.0 to
1.4.0, which violates semantic versioning and will cause issues with release
management tools. Update the version value to a forward version number that is
higher than the previously published 1.5.0 (such as 1.5.1 or 1.6.0, depending on
whether this is a patch or minor release) to maintain proper version ordering
for releases.

In `@src/utils/index.ts`:
- Around line 167-168: The call to executePendingJobs() on the vm.runtime object
returns a result containing a QuickJSHandle that must be properly disposed to
prevent VM memory leaks. Capture the return value from executePendingJobs() and
check if it contains an error handle, then ensure the handle is disposed by
calling the appropriate disposal method on the returned result before
proceeding. This will prevent memory leaks when deadline interrupts or job
errors occur.
- Around line 114-120: The issue is a race condition where the snapshot read via
GlobalStateProvider.getInstance().getSharedStateCopy() occurs before an await
statement, allowing concurrent invocations to modify shared state via
setSharedState() while control is yielded, causing stale state to overwrite
concurrent updates when execution resumes. Move the entire try-catch block
containing the getSharedStateCopy() call to execute after the await
getQuickJSModule() statement completes, so that the snapshot is captured after
the async module resolution rather than before it. This closes the interleaving
window and ensures the snapshot reflects current state at the point it will be
used.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a2509111-22a8-4f66-8744-b17a2a20643b

📥 Commits

Reviewing files that changed from the base of the PR and between 381f6b9 and 2f32940.

⛔ Files ignored due to path filters (5)
  • dist/components/proxy-middleware/rule_action_processor/processors/modify_request_processor.js is excluded by !**/dist/**
  • dist/components/proxy-middleware/rule_action_processor/processors/modify_response_processor.js is excluded by !**/dist/**
  • dist/utils/index.d.ts is excluded by !**/dist/**
  • dist/utils/index.js is excluded by !**/dist/**
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • package.json
  • src/components/proxy-middleware/rule_action_processor/processors/modify_request_processor.js
  • src/components/proxy-middleware/rule_action_processor/processors/modify_response_processor.js
  • src/utils/index.ts

Comment thread package.json Outdated
Comment thread src/utils/index.ts
Comment thread src/utils/index.ts Outdated
dinex-dev and others added 3 commits June 24, 2026 11:44
QuickJS is a bare ECMAScript engine — it lacks the Web/Node globals rule code
commonly expects. Add them as PURE-JS shims that run inside the sandbox, built
only from QuickJS built-ins so no host object crosses the boundary (same safety
model as atob/btoa; verified require/process stay undefined in-sandbox):

- URL / URLSearchParams (regex-based parser; common cases — protocol/host/port/
  path/search/hash/origin, searchParams, basic relative-base resolution)
- TextEncoder / TextDecoder (UTF-8)
- structuredClone (deep clone, preserves Date/Map/Set, handles cycles)
- crypto.randomUUID / crypto.getRandomValues

NOTE on crypto: this is a Math.random-based STOPGAP — NOT cryptographically
secure (no entropy source in the WASM realm). Fine for ids/non-security random;
secure crypto should be a host bridge (follow-up), matching
@requestly/sandbox-node's byte-identical-to-host crypto approach.

Aligns with the API client's QuickJS sandbox model: pure-JS shims for
computation-only APIs, host bridge reserved for capability APIs (crypto/fetch).
URL is hand-rolled (sandbox-node doesn't expose URL — uses a regex internally —
so this is a superset of theirs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The QuickJS sandbox commit was assembled from a checkpoint based on an older
master and inadvertently reverted package.json/package-lock from 1.5.0 to 1.4.0.
Restore to 1.5.0 so the branch's only package.json delta vs master is the added
quickjs-emscripten deps. Dependency versions left untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Read the $sharedState snapshot after the last await so the snapshot->eval->
  setSharedState read-modify-write is atomic w.r.t. the event loop. Reading it
  before the await let a concurrent executeUserFunction commit in the gap, whose
  update this call's stale snapshot would then clobber (last-writer-wins).
- Capture executePendingJobs() result and dispose its error handle (carried on
  a job error / deadline interrupt) instead of discarding it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant