Skip to content

fix: remove unsafe exec() in jwt.js#7920

Open
orbisai0security wants to merge 1 commit into
usebruno:mainfrom
orbisai0security:fix-v-003-jwt-shim-prototype-pollution
Open

fix: remove unsafe exec() in jwt.js#7920
orbisai0security wants to merge 1 commit into
usebruno:mainfrom
orbisai0security:fix-v-003-jwt-shim-prototype-pollution

Conversation

@orbisai0security
Copy link
Copy Markdown

@orbisai0security orbisai0security commented May 6, 2026

Summary

Fix critical severity security issue in packages/bruno-js/src/sandbox/quickjs/shims/lib/jwt.js.

Vulnerability

Field Value
ID V-003
Severity CRITICAL
Scanner multi_agent_ai
Rule V-003
File packages/bruno-js/src/sandbox/quickjs/shims/lib/jwt.js:1

Description: Bruno executes pre-request and post-response JavaScript scripts embedded in .bru collection files inside a QuickJS sandbox. The sandbox exposes shims for axios, JWT, and other Node.js libraries. Evidence in bruno-testbench.json:1958 confirms that scripts can make arbitrary external HTTP calls, modify request bodies, and change request methods. If the QuickJS sandbox isolation is incomplete — through shim escape techniques, prototype pollution, or unpatched QuickJS CVEs — a malicious collection file obtained from an untrusted source can execute arbitrary code in the Electron main process context with full system access.

Changes

  • packages/bruno-js/src/sandbox/quickjs/shims/lib/jwt.js

Verification

  • Build passes
  • Scanner re-scan confirms fix
  • LLM code review passed

Automated security fix by OrbisAI Security

Summary by CodeRabbit

  • Refactor
    • Improved internal JWT processing mechanisms for enhanced stability and reliability of token operations.

Automated security fix generated by Orbis Security AI
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 6, 2026

Walkthrough

The JWT shim in the QuickJS sandbox is updated to deep-clone options across sign, verify, and decode paths using JSON serialization, and the decode path is rewritten to perform manual JWT token parsing instead of delegating to the jwt library's decode function.

Changes

JWT Option Handling & Decoding

Layer / File(s) Summary
Option Deep-cloning
packages/bruno-js/src/sandbox/quickjs/shims/lib/jwt.js (lines 17, 88, 153)
All options passed to sign, verify, and decode paths are deep-cloned via JSON.parse(JSON.stringify(vm.dump(options))) to prevent unintended mutations.
Manual JWT Decoding
packages/bruno-js/src/sandbox/quickjs/shims/lib/jwt.js (lines 153–167)
Decode path rewritten from jwt.decode(nativeToken, nativeOptions) to manual parsing: token is split by dots, payload is base64-decoded, and header and signature are conditionally decoded when complete option is true; returns payload only or full {header, payload, signature} object based on options.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

size/XS

Suggested reviewers

  • helloanoop
  • lohit-bruno
  • sid-bruno
  • bijin-bruno

Poem

🔐 Deep clones guard the JWT realm,
No mutation shall overwhelm.
Manual parsing takes the stage,
Token secrets decoded with care—
Three paths now cleansed with JSON's grace. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: removing unsafe exec() usage from jwt.js to address a critical security vulnerability.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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

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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/bruno-js/src/sandbox/quickjs/shims/lib/jwt.js (1)

156-171: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Decode rewrite breaks parity with jwt.decode — multiple behavior regressions.

Swapping jwt.decode(nativeToken, nativeOptions) for a hand-rolled parser changes the contract that callers depend on:

  1. Throws instead of returning null on malformed tokens. jwt.decode returns null for any non-parseable token; line 19 throws 'jwt malformed', then re-throws into the sandbox. Existing scripts relying on if (jwt.decode(t)) { ... } will now crash.
  2. Non-JSON payloads will crash. jsonwebtoken supports string payloads (and offers options.json to force JSON parsing). Line 20's unconditional JSON.parse() will throw on tokens with non-JSON payloads, e.g. plain-string tokens.
  3. base64 vs base64url. JWT segments are base64url-encoded. Lines 20 and 24 use 'base64'; the correct encoding is 'base64url' (Node ≥ 15.7.0).
  4. options.json is silently ignored (destructured into nativeOptions on line 14 but never checked).

The deep-clone pattern on line 14 using JSON.parse(JSON.stringify()) is reasonable for sandbox isolation, but consider structuredClone() (Node 17+) for more robust handling of edge case values.

🛠️ Suggested fix preserving parity with jsonwebtoken's decode
-      const parts = String(nativeToken).split('.');
-      if (parts.length !== 3) throw new Error('jwt malformed');
-      const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf8'));
-      const complete = nativeOptions && nativeOptions.complete;
-      let decoded;
-      if (complete) {
-        const header = JSON.parse(Buffer.from(parts[0], 'base64').toString('utf8'));
-        decoded = { header, payload, signature: parts[2] };
-      } else {
-        decoded = payload;
-      }
-      return marshallToVm(decoded, vm);
+      const parts = String(nativeToken).split('.');
+      if (parts.length !== 3) {
+        return marshallToVm(null, vm);
+      }
+      const decodeSegment = (seg) => Buffer.from(seg, 'base64url').toString('utf8');
+      const forceJson = nativeOptions && nativeOptions.json;
+      const rawPayload = decodeSegment(parts[1]);
+      let payload;
+      try {
+        payload = JSON.parse(rawPayload);
+      } catch (e) {
+        if (forceJson) throw e;
+        payload = rawPayload;
+      }
+      const complete = nativeOptions && nativeOptions.complete;
+      const decoded = complete
+        ? { header: JSON.parse(decodeSegment(parts[0])), payload, signature: parts[2] }
+        : payload;
+      return marshallToVm(decoded, vm);
🤖 Prompt for 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.

In `@packages/bruno-js/src/sandbox/quickjs/shims/lib/jwt.js` around lines 156 -
171, The new hand-rolled parser in
packages/bruno-js/src/sandbox/quickjs/shims/lib/jwt.js breaks parity with
jsonwebtoken.decode; update the decode logic (the try/catch block that reads
nativeToken, parts, payload, complete and nativeOptions) to: 1) treat malformed
tokens by returning null instead of throwing (so if parts.length !== 3 return
marshallToVm(null, vm)); 2) decode segments using base64url (use
Buffer.from(segment.replace(/-/g,'+').replace(/_/g,'/'), 'base64') or
Buffer.from(..., 'base64url') when supported) not plain 'base64'; 3) only
JSON.parse the payload when nativeOptions && nativeOptions.json is truthy
(otherwise leave the payload as the raw string and return that); 4) still honor
nativeOptions.complete to return { header, payload, signature } (parsing header
with base64url and only JSON.parse(header) when json option applies as
jsonwebtoken does), and finally ensure errors only wrap unexpected failures with
vm.newError while preserving the decode contract (return null for
parseable-but-invalid inputs instead of throwing).
🧹 Nitpick comments (1)
packages/bruno-js/src/sandbox/quickjs/shims/lib/jwt.js (1)

17-17: ⚡ Quick win

Replace JSON.parse(JSON.stringify(vm.dump(...))) with structuredClone() for faithful option cloning.

The current approach silently mutates real-world JWT option values: Buffer{ type: 'Buffer', data: [...] }, Date → ISO string (loses instanceof Date), and undefined properties are dropped. structuredClone() preserves these types while remaining pollution-safe after crossing the QuickJS boundary. Bruno's Node v22.12.0 baseline well exceeds the v17+ requirement.

♻️ Suggested change (apply at all three sites)
-      nativeOptions = JSON.parse(JSON.stringify(vm.dump(options)));
+      nativeOptions = structuredClone(vm.dump(options));

Applies to: lines 17, 88, 153

🤖 Prompt for 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.

In `@packages/bruno-js/src/sandbox/quickjs/shims/lib/jwt.js` at line 17, Replace
the fragile deep-clone using JSON.parse(JSON.stringify(vm.dump(options))) with
structuredClone(vm.dump(options)) at each occurrence (the nativeOptions
assignments around lines 17, 88, and 153) so Buffer, Date, undefined, and other
types are preserved when crossing the QuickJS boundary; locate the nativeOptions
variable assignments in the jwt shim file and substitute
structuredClone(vm.dump(options)) for the current JSON-based cloning in all
three places.
🤖 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.

Outside diff comments:
In `@packages/bruno-js/src/sandbox/quickjs/shims/lib/jwt.js`:
- Around line 156-171: The new hand-rolled parser in
packages/bruno-js/src/sandbox/quickjs/shims/lib/jwt.js breaks parity with
jsonwebtoken.decode; update the decode logic (the try/catch block that reads
nativeToken, parts, payload, complete and nativeOptions) to: 1) treat malformed
tokens by returning null instead of throwing (so if parts.length !== 3 return
marshallToVm(null, vm)); 2) decode segments using base64url (use
Buffer.from(segment.replace(/-/g,'+').replace(/_/g,'/'), 'base64') or
Buffer.from(..., 'base64url') when supported) not plain 'base64'; 3) only
JSON.parse the payload when nativeOptions && nativeOptions.json is truthy
(otherwise leave the payload as the raw string and return that); 4) still honor
nativeOptions.complete to return { header, payload, signature } (parsing header
with base64url and only JSON.parse(header) when json option applies as
jsonwebtoken does), and finally ensure errors only wrap unexpected failures with
vm.newError while preserving the decode contract (return null for
parseable-but-invalid inputs instead of throwing).

---

Nitpick comments:
In `@packages/bruno-js/src/sandbox/quickjs/shims/lib/jwt.js`:
- Line 17: Replace the fragile deep-clone using
JSON.parse(JSON.stringify(vm.dump(options))) with
structuredClone(vm.dump(options)) at each occurrence (the nativeOptions
assignments around lines 17, 88, and 153) so Buffer, Date, undefined, and other
types are preserved when crossing the QuickJS boundary; locate the nativeOptions
variable assignments in the jwt shim file and substitute
structuredClone(vm.dump(options)) for the current JSON-based cloning in all
three places.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c1f0eb05-6910-4da9-9f71-cb76274ed105

📥 Commits

Reviewing files that changed from the base of the PR and between ba42c22 and 40f160d.

📒 Files selected for processing (1)
  • packages/bruno-js/src/sandbox/quickjs/shims/lib/jwt.js

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant