From e7918600f839c1be778388a6c4a66e49fce71973 Mon Sep 17 00:00:00 2001 From: John Bolliger Date: Wed, 23 Sep 2026 19:01:06 -0700 Subject: [PATCH] Build the js gem's constants without JS.eval JS::Undefined, JS::Null, JS::True, JS::False, Array#to_js and Hash#to_js built their values with JS.eval, which runs Function(). A page whose Content-Security-Policy has no 'unsafe-eval' blocks Function(), so `require "js"` raised EvalError at js.rb:46 and nothing using the gem could load. Build the same values from globalThis, JSON.parse and the existing true/false#to_js instead. Array#to_js calls Array() rather than JS.global[:Array].new, because JS::Object#new itself calls Array#to_js. JS.eval itself is unchanged; it still needs 'unsafe-eval'. Co-Authored-By: Claude Opus 5.5 --- packages/gems/js/lib/js.rb | 10 +++++---- packages/gems/js/lib/js/array.rb | 4 +++- packages/gems/js/lib/js/hash.rb | 3 ++- .../ruby-wasm-wasi/test/no_eval.test.js | 22 +++++++++++++++++++ 4 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 packages/npm-packages/ruby-wasm-wasi/test/no_eval.test.js diff --git a/packages/gems/js/lib/js.rb b/packages/gems/js/lib/js.rb index 3564065f8..116ed69fd 100644 --- a/packages/gems/js/lib/js.rb +++ b/packages/gems/js/lib/js.rb @@ -43,8 +43,10 @@ # end # module JS - Undefined = JS.eval("return undefined") - Null = JS.eval("return null") + # These constants avoid JS.eval so that `require "js"` works under a + # Content-Security-Policy without 'unsafe-eval'. + Undefined = JS.global[:undefined] + Null = JS.global[:JSON].call(:parse, "null") # A boolean value in JavaScript is always a JS::Object instance from Ruby's point of view. # If we use the boolean value returned by a JavaScript function as the condition for an if expression in Ruby, @@ -67,8 +69,8 @@ module JS # if searchParams.has('phrase') == JS::True # ... # end - True = JS.eval("return true;") - False = JS.eval("return false;") + True = true.to_js + False = false.to_js class PromiseScheduler def initialize(loop) diff --git a/packages/gems/js/lib/js/array.rb b/packages/gems/js/lib/js/array.rb index 214bb3cd0..c16c8872c 100644 --- a/packages/gems/js/lib/js/array.rb +++ b/packages/gems/js/lib/js/array.rb @@ -1,7 +1,9 @@ class Array # Convert Ruby array to JavaScript array def to_js - new_array = JS.eval("return []") + # Array() instead of JS.eval, which a CSP without 'unsafe-eval' blocks. + # Not JS.global[:Array].new: JS::Object#new calls Array#to_js. + new_array = JS.global.call(:Array) # NOTE: This method call implicitly convert element to JS object by to_js new_array.push *self new_array diff --git a/packages/gems/js/lib/js/hash.rb b/packages/gems/js/lib/js/hash.rb index 3a0d061bd..5d827a898 100644 --- a/packages/gems/js/lib/js/hash.rb +++ b/packages/gems/js/lib/js/hash.rb @@ -1,7 +1,8 @@ class Hash # Convert a hash to a JavaScript object def to_js - new_object = JS.eval("return {}") + # Object() instead of JS.eval, which a CSP without 'unsafe-eval' blocks. + new_object = JS.global.call(:Object) self.each { |key, value| new_object[key] = value } new_object end diff --git a/packages/npm-packages/ruby-wasm-wasi/test/no_eval.test.js b/packages/npm-packages/ruby-wasm-wasi/test/no_eval.test.js new file mode 100644 index 000000000..88f63d40b --- /dev/null +++ b/packages/npm-packages/ruby-wasm-wasi/test/no_eval.test.js @@ -0,0 +1,22 @@ +import { initRubyVM } from "./init"; +import { describe, test, expect, vi, afterEach } from "vitest" + +// A page whose Content-Security-Policy lacks 'unsafe-eval' blocks Function(), +// which JS.eval uses. Loading the js gem and converting values must not need it. +describe("without Function()", () => { + afterEach(() => vi.unstubAllGlobals()); + + test("require 'js' and to_js work", async () => { + vi.stubGlobal("Function", () => { + throw new EvalError("Function() is blocked"); + }); + const vm = await initRubyVM(); + const result = vm.eval(` + require "js" + blocked = begin; JS.eval("return 1"); false; rescue JS::Error; true; end + [blocked, JS::Undefined.typeof, JS::Null.inspect, JS::True.to_s, JS::False.to_s, + [1, 2].to_js[:length].to_i, { a: 1 }.to_js[:a].to_i].inspect + `); + expect(result.toString()).toBe('[true, "undefined", "null", "true", "false", 2, 1]'); + }); +});