Skip to content

Conversation

@bbopen
Copy link

@bbopen bbopen commented Dec 30, 2025

Summary

  • Removes unsafe eval() calls in the query editor that executed user-provided JavaScript
  • Replaces with quickjs-emscripten WASM sandbox for complete memory isolation
  • Adds blocked pattern detection for dangerous operations (fetch, XMLHttpRequest, eval, etc.)
  • Implements configurable timeout via VITE_QUERY_TIMEOUT environment variable
  • Gracefully disables query feature when WASM sandbox is unavailable

Changes

File Change
dashboard/frontend/package.json Added quickjs-emscripten dependency
dashboard/frontend/src/helpers.ts Added configurable QUERY_TIMEOUT_MS
dashboard/frontend/src/services/sandbox-service.ts New sandbox service with WASM isolation
dashboard/frontend/src/pages/databases/query.tsx Replaced eval() with sandbox execution
dashboard/frontend/tests/sandbox.test.ts Security test suite for sandbox

Security Model

  1. Pre-execution validation: Blocked patterns detected before code reaches sandbox
  2. WASM isolation: User code runs in isolated QuickJS context with no host access
  3. Timeout enforcement: Prevents infinite loops with configurable timeout
  4. Fail-safe: Query feature disabled with clear message if WASM unavailable

Test plan

  • Lint passes (only console warnings remain)
  • Build succeeds
  • All 128 test files pass (1781 tests)
  • Manual testing of query editor functionality
  • Verify sandbox blocks malicious patterns
  • Verify timeout enforcement works

Blocked Patterns

The following patterns are detected and blocked:

  • fetch() / XMLHttpRequest - network access
  • import() / require() - dynamic code loading
  • eval() / Function() - code generation
  • .constructor() - prototype chain escape
  • window / document / globalThis - DOM/global access
  • localStorage / sessionStorage - storage access

Summary by CodeRabbit

  • Chores

    • Added a new runtime dependency to support WASM execution and updated build optimization to exclude it.
  • New Features

    • Introduced a sandboxed WASM execution environment for map functions with pre-execution validation.
    • UI now shows sandbox initialization and error states; query execution respects a configurable timeout.
  • Tests

    • Added comprehensive security and timeout tests for sandboxed code execution.

✏️ Tip: You can customize this high-level summary in your review settings.

- Add quickjs-emscripten for isolated code execution
- Create sandbox-service with timeout support
- Block dangerous patterns before execution
- Disable feature gracefully when WASM unavailable
- Add security test suite

Closes ROBUST-01
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 30, 2025

Pre-merge checks and finishing touches

✅ Passed checks (3 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 objective of the changeset: replacing unsafe eval() usage with a WASM sandbox for secure query execution.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings

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
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.

Actionable comments posted: 4

🧹 Nitpick comments (2)
dashboard/frontend/tests/sandbox.test.ts (1)

159-169: Consider using it.skipIf for cleaner conditional test skipping.

The early return pattern works but silently passes tests when the sandbox is unavailable. Using Vitest's it.skipIf or it.runIf would make skipped tests visible in test output.

🔎 Suggested pattern
// At the top of the describe block:
const sandboxReady = await initSandbox();

// Then for each test:
it.runIf(sandboxReady)("executes valid map functions", async () => {
  const results = await executeMapFn("(doc, emit) => { emit(doc._id, doc); }", testDocs);
  expect(results).toHaveLength(3);
  expect(results[0].key).toBe("doc1");
});
dashboard/frontend/src/pages/databases/query.tsx (1)

187-200: Add cancellation for async effect to prevent state updates on unmounted component.

If the component unmounts while executeMapFn is pending, the .then() handlers will still attempt to call setQueryResults and setIsLoading, potentially causing React warnings.

🔎 Proposed fix with cleanup
   useEffect(() => {
+    let cancelled = false;
+
     if (allDocs.length === 0) {
       setQueryResults([]);
       setIsLoading(false);
       return;
     }

     setIsLoading(true);
     setQueryError(null);

     executeMapFn(mapFn, allDocs)
       .then((results: MapResult[]) => {
+        if (cancelled) return;
         // Extract documents from results
         const docs = results
           .map((r: MapResult) => r.value)
           .filter((v: unknown): v is SandboxDocument => v !== null && typeof v === "object");
         setQueryResults(docs);
         setIsLoading(false);
       })
       .catch((error: Error) => {
+        if (cancelled) return;
         setQueryError(error.message);
         setQueryResults([]);
         setIsLoading(false);
       });
+
+    return () => { cancelled = true; };
   }, [mapFn, allDocs]);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 68b2b88 and 94b6c96.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • dashboard/frontend/package.json
  • dashboard/frontend/src/helpers.ts
  • dashboard/frontend/src/pages/databases/query.tsx
  • dashboard/frontend/src/services/sandbox-service.ts
  • dashboard/frontend/tests/sandbox.test.ts
🔇 Additional comments (8)
dashboard/frontend/tests/sandbox.test.ts (2)

208-222: Timeout test looks good but may be flaky on slow CI.

The 100ms timeout with a 5s test timeout should work, but consider adding a comment explaining why these specific values were chosen. The while(true) infinite loop is an effective test case.


23-106: Comprehensive blocked patterns coverage.

The test suite thoroughly covers all dangerous patterns: network access (fetch, XMLHttpRequest), dynamic code execution (eval, Function, import, require), DOM access (window, document), and storage (localStorage, sessionStorage, globalThis). Good security test coverage.

dashboard/frontend/src/pages/databases/query.tsx (1)

79-101: Good graceful degradation for sandbox unavailability.

The loading and error states provide clear user feedback when the WASM sandbox is initializing or fails to load. The error message appropriately suggests WebAssembly support requirements.

dashboard/frontend/src/services/sandbox-service.ts (4)

117-129: Syntax validation using Function constructor is acceptable here.

Using new Function() purely for syntax validation (without executing the result) is a safe pattern. The constructed function is never called, so no code execution occurs.


183-195: Code injection via docsJson is safe, but validate the interpolation.

The ${docsJson} and ${code} interpolations are:

  • docsJson: Safe because JSON.stringify produces valid JSON that cannot escape the string context
  • code: Already validated by validateCode() before reaching this point

The sandbox isolation provides defense-in-depth even if validation were bypassed.


164-211: Good handle disposal pattern prevents memory leaks.

All QuickJS handles (emitHandle, result.error, result.value) are properly disposed, and vm.dispose() is called in a finally block ensuring cleanup even on errors.


186-187: The execution code uses globalThis.emit to access the emit function set on vm.global at line 176.

In QuickJS (and standard JavaScript), globalThis is the standardized reference to the global object, so this works correctly. However, since emit is explicitly set on vm.global, using that directly in the execution code would be more explicit and clearer about the intent. Consider accessing the global object consistently—either always through globalThis or always through vm.global—to improve code clarity.

dashboard/frontend/package.json (1)

49-49: The specified version does not exist for this package.

The dependency "quickjs-emscripten": "^0.31.0" cannot be installed. Version 0.31.0 is not available for the unscoped quickjs-emscripten package; the latest published versions are 0.23.0 → 0.25.x → 0.26.0. Version 0.31.0 exists only for the quickjs-emscripten-core package (different package).

Update the version to an available release (e.g., "^0.26.0") or change the package name to quickjs-emscripten-core if that's the intended dependency.

⛔ Skipped due to learnings
Learnt from: mabels
Repo: fireproof-storage/fireproof PR: 549
File: package-use-fireproof.json:34-34
Timestamp: 2025-02-25T05:31:43.165Z
Learning: The "!" character in version requirements (like ">=18.0.0!") in package.json files is intentional because these files are post-processed, and it's not a typo.

Comment on lines 177 to 201
useEffect(() => {
if (allDocs.length === 0) {
setQueryResults([]);
setIsLoading(false);
return;
}

setIsLoading(true);
setQueryError(null);

executeMapFn(mapFn, allDocs)
.then((results: MapResult[]) => {
// Extract documents from results
const docs = results
.map((r: MapResult) => r.value)
.filter((v: unknown): v is SandboxDocument => v !== null && typeof v === "object");
setQueryResults(docs);
setIsLoading(false);
})
.catch((error: Error) => {
setQueryError(error.message);
setQueryResults([]);
setIsLoading(false);
});
}, [mapFn, allDocs]);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd "query.tsx" --type f

Repository: fireproof-storage/fireproof

Length of output: 119


🏁 Script executed:

# Find the query.tsx file and read its content to understand allDocs definition
head -250 dashboard/frontend/src/pages/databases/query.tsx | tail -100

Repository: fireproof-storage/fireproof

Length of output: 3151


🏁 Script executed:

# Search for allDocs definition in query.tsx
rg -n "allDocs" dashboard/frontend/src/pages/databases/query.tsx

Repository: fireproof-storage/fireproof

Length of output: 341


🏁 Script executed:

# Search for useLiveQuery usage in the file
rg -n -B2 -A5 "useLiveQuery" dashboard/frontend/src/pages/databases/query.tsx

Repository: fireproof-storage/fireproof

Length of output: 765


🏁 Script executed:

# Search for useLiveQuery implementation
rg -n -A20 "export.*useLiveQuery|function useLiveQuery" --type=ts --type=tsx

Repository: fireproof-storage/fireproof

Length of output: 98


🏁 Script executed:

# Look in useFireproof hook or similar
rg -n "useFireproof" dashboard/frontend/src/pages/databases/query.tsx | head -5

Repository: fireproof-storage/fireproof

Length of output: 170


🏁 Script executed:

# Search more broadly for useLiveQuery definition
fd --type f "\.ts$|\.tsx$" | xargs rg -l "useLiveQuery" | head -10

Repository: fireproof-storage/fireproof

Length of output: 512


🏁 Script executed:

# Check useLiveQuery implementation
cat use-fireproof/base/react/use-live-query.ts

Repository: fireproof-storage/fireproof

Length of output: 1905


🏁 Script executed:

# Also check useFireproof to understand the hook structure
head -100 use-fireproof/base/react/use-fireproof.ts

Repository: fireproof-storage/fireproof

Length of output: 2030


Consider stabilizing the dependency to avoid unnecessary re-executions.

The useEffect depends on allDocs, which is a new array reference on every database update (the useMemo dependency on allDocsResult.docs doesn't prevent this, since useLiveQuery returns a new array reference each time). This causes executeMapFn to re-run even when documents haven't actually changed. Since executeMapFn involves expensive sandbox code execution, consider using allDocsResult.docs.length in the dependency array, or refactor to detect actual document changes (e.g., by comparing IDs) rather than re-executing on every array reference change.

🤖 Prompt for AI Agents
In dashboard/frontend/src/pages/databases/query.tsx around lines 177 to 201, the
useEffect currently depends on allDocs (which gets a new array reference on
every update) causing executeMapFn to rerun unnecessarily; change the dependency
to a stable signal such as allDocs.length or a derived value of document IDs
(e.g., a memoized string/join of IDs) so the effect only reruns when the actual
set of documents changes, and ensure you compute that derived dependency with
useMemo/useRef outside the effect to avoid re-creating it each render.

Comment on lines +48 to +61
const BLOCKED_PATTERNS: { pattern: RegExp; description: string }[] = [
{ pattern: /\bfetch\s*\(/, description: "fetch() calls are not allowed" },
{ pattern: /\bXMLHttpRequest\b/, description: "XMLHttpRequest is not allowed" },
{ pattern: /\bimport\s*\(/, description: "Dynamic imports are not allowed" },
{ pattern: /\brequire\s*\(/, description: "require() calls are not allowed" },
{ pattern: /\beval\s*\(/, description: "eval() is not allowed" },
{ pattern: /\bFunction\s*\(/, description: "Function constructor is not allowed" },
{ pattern: /\.constructor\s*\(/, description: "Constructor access is not allowed" },
{ pattern: /\bwindow\b/, description: "window object access is not allowed" },
{ pattern: /\bdocument\b/, description: "document object access is not allowed" },
{ pattern: /\blocalStorage\b/, description: "localStorage access is not allowed" },
{ pattern: /\bsessionStorage\b/, description: "sessionStorage access is not allowed" },
{ pattern: /\bglobalThis\b/, description: "globalThis access is not allowed" },
];
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Regex patterns may be bypassable with whitespace or bracket notation.

Several patterns can be circumvented:

  • fetch\n(url) bypasses /\bfetch\s*\(/ (newline not matched by \s*)
  • obj["constructor"]() bypasses /\.constructor\s*\(/
  • Comments or string splitting could hide patterns

Consider AST-based validation for stronger guarantees, or at minimum, normalize whitespace before checking.

🔎 Examples of bypasses
// Bypass fetch pattern with newline:
(doc, emit) => { fetch
("http://evil.com"); }

// Bypass constructor pattern with bracket notation:
(doc, emit) => { doc["constructor"]("code")(); }

// Bypass with template literal:
(doc, emit) => { const f = "fe" + "tch"; globalThis[f]("url"); }

Mitigations:

  1. Normalize code (remove comments, collapse whitespace) before pattern matching
  2. Use an AST parser for more robust detection
  3. Since code runs in QuickJS sandbox anyway, these patterns would fail at runtime - but pre-validation gives clearer errors
🤖 Prompt for AI Agents
In dashboard/frontend/src/services/sandbox-service.ts around lines 48 to 61, the
current RegExp checks are bypassable (e.g. newlines, bracket notation,
string/concat obfuscation and comments) so update validation to be more robust:
either parse the input into an AST (recommended) and detect prohibited
identifiers/call expressions (fetch, XMLHttpRequest, import(), require(), eval,
Function, constructor access,
window/document/globalThis/localStorage/sessionStorage) or, if you must keep
regex checks, first strip comments and string/template literals and
normalize/compact all whitespace (convert newlines to spaces) and add additional
patterns to catch bracket notation (e.g. \[\s*["']constructor["']\s*\]) and
dynamic whitespace/newlines before parentheses; perform checks against the
normalized source and return clear diagnostic messages for detected constructs.

- Use QuickJS interrupt handler for proper timeout enforcement
- Fix Vite config to serve WASM files correctly
- Add proper useEffect cleanup to prevent state updates after unmount
- Improve test skipping pattern with top-level await
- Better VITE_QUERY_TIMEOUT parsing with explicit validation
@bbopen
Copy link
Author

bbopen commented Dec 30, 2025

Dependency Analysis: quickjs-emscripten

Bundle Impact

  • Release WASM file: ~500KB - 1MB
  • This is a dashboard-only dependency, not in the core library

Alternatives Considered

Approach Security Bundle Size Complexity Offline-Compatible
quickjs-emscripten (chosen) ✅ True isolation ~1MB Medium ✅ Yes
Web Worker + CSP ⚠️ Partial (has fetch/indexedDB) 0 High ✅ Yes
Sandboxed iframe ⚠️ Partial (complex setup) 0 High ✅ Yes
Server-side execution ✅ True isolation 0 High ❌ No
AST analysis + eval ❌ Bypassable 0 Low ✅ Yes
Remove query editor ✅ N/A -current None ✅ Yes

Recommendation

If the query editor feature is valuable, quickjs-emscripten is the correct security solution because:

  1. Fireproof is offline-first - server-side execution breaks this philosophy
  2. True sandboxing - regex/AST validation can be bypassed with creative encoding
  3. Dashboard context - 1MB is acceptable for an admin tool (not core library)

Alternative: Remove the feature?

If the query editor isn't heavily used, removing it entirely would be simpler than maintaining sandbox code. Happy to adjust the approach based on your preference.

Copy link
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.

Actionable comments posted: 0

🧹 Nitpick comments (1)
dashboard/frontend/src/pages/databases/query.tsx (1)

177-211: Memory leak fixed; consider optimizing effect dependencies.

The cancelled flag correctly prevents state updates after unmount, addressing the memory leak concern from the learnings. However, the dependency on allDocs may trigger unnecessary re-executions of the expensive executeMapFn operation. Since useLiveQuery returns a new array reference on each call, allDocs is recreated even when the underlying documents haven't changed, causing the effect to re-run.

Consider using a more stable dependency such as a memoized hash of document IDs or a revision counter to avoid redundant sandbox executions when documents haven't actually changed.

🔎 Potential optimization
  const allDocsResult = useLiveQuery("_id");
  const allDocs = useMemo(() => allDocsResult.docs.filter((doc) => doc) as SandboxDocument[], [allDocsResult.docs]);
+ // Create stable dependency based on document IDs
+ const docsSignature = useMemo(() => allDocs.map(d => d._id).join(','), [allDocs]);

  useEffect(() => {
    let cancelled = false;
    // ... rest of effect logic
-  }, [mapFn, allDocs]);
+  }, [mapFn, docsSignature, allDocs]);

Note: Keep allDocs in the dependency array for the actual data, but docsSignature provides a stable comparison point to prevent re-execution when only the array reference changes.

Based on learnings from past reviews about useEffect dependencies and stability.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 94b6c96 and a8260e2.

📒 Files selected for processing (5)
  • dashboard/frontend/src/helpers.ts
  • dashboard/frontend/src/pages/databases/query.tsx
  • dashboard/frontend/src/services/sandbox-service.ts
  • dashboard/frontend/tests/sandbox.test.ts
  • dashboard/frontend/vite.config.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • dashboard/frontend/src/services/sandbox-service.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-10T10:00:01.115Z
Learnt from: mabels
Repo: fireproof-storage/fireproof PR: 1130
File: use-fireproof/react/use-attach.ts:160-185
Timestamp: 2025-09-10T10:00:01.115Z
Learning: In use-fireproof/react/use-attach.ts, there's a potential memory leak where the onTokenChange callback can call setAttachState after component unmount. The fix requires adding cleanup to the useEffect with a disposed flag to guard state updates, and ideally refactoring WebToCloudCtx.onTokenChange to return an unsubscribe function.

Applied to files:

  • dashboard/frontend/src/pages/databases/query.tsx
📚 Learning: 2025-09-10T09:53:26.979Z
Learnt from: mabels
Repo: fireproof-storage/fireproof PR: 1130
File: use-fireproof/react/use-attach.ts:186-199
Timestamp: 2025-09-10T09:53:26.979Z
Learning: In use-fireproof/react/use-attach.ts, the current attach flow calls database.attach immediately via KeyedResolvOnce, which can run before a token is available and won't re-run after resetToken. The attach logic should be moved into the onTokenChange success path to ensure it only runs when a valid token exists and can re-attach after token reset.

Applied to files:

  • dashboard/frontend/src/pages/databases/query.tsx
🧬 Code graph analysis (1)
dashboard/frontend/tests/sandbox.test.ts (1)
dashboard/frontend/src/services/sandbox-service.ts (5)
  • initSandbox (77-91)
  • validateCode (110-130)
  • SandboxDocument (29-32)
  • executeMapFn (142-213)
  • isSandboxAvailable (99-101)
🔇 Additional comments (4)
dashboard/frontend/src/helpers.ts (1)

30-48: LGTM! NaN guard properly implemented.

The timeout configuration logic correctly validates the parsed value using Number.isFinite() and the >= 0 check, which handles all edge cases including NaN from non-numeric strings, negative values, and the intentional zero-to-disable behavior. The past review concern has been fully addressed.

dashboard/frontend/vite.config.ts (1)

49-51: LGTM! Correct optimization configuration for WASM package.

Excluding quickjs-emscripten from dependency optimization is the proper approach for WASM-based packages, ensuring they load correctly at runtime rather than being pre-bundled.

dashboard/frontend/tests/sandbox.test.ts (1)

1-204: LGTM! Comprehensive security test coverage.

The test suite thoroughly validates the sandbox security model with proper conditional execution when WASM is unavailable. The blocked pattern tests cover all critical attack vectors (fetch, eval, constructor escapes, DOM access, storage APIs), and the valid pattern tests ensure legitimate queries still work. The cleanup and error handling paths are also well-tested.

dashboard/frontend/src/pages/databases/query.tsx (1)

1-101: LGTM! Robust sandbox initialization and error handling.

The sandbox initialization flow properly handles loading states and failures with clear user-facing messages. The validation in runTempQuery correctly uses the sandboxed validateCode instead of eval, providing security without sacrificing functionality. The progressive enhancement approach gracefully degrades when WASM is unavailable.

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