diff --git a/Core/Node-API-JSI/Include/napi/napi-inl.h b/Core/Node-API-JSI/Include/napi/napi-inl.h index 14fb745c..d9473742 100644 --- a/Core/Node-API-JSI/Include/napi/napi-inl.h +++ b/Core/Node-API-JSI/Include/napi/napi-inl.h @@ -2315,12 +2315,14 @@ ObjectWrap::DefineClass(napi_env env, prototype; if (p.staticVoidMethod != nullptr) { + descriptor.setProperty(rt, "writable", jsi::Value{(p.attributes & napi_writable) != 0}); descriptor.setProperty(rt, "value", jsi::Function::createFromHostFunction(rt, name, 0, [env, method{p.staticVoidMethod}, data{p.data}](jsi::Runtime& /*rt*/, const jsi::Value& thisVal, const jsi::Value* args, size_t count) -> jsi::Value { (*method)({env, thisVal, args, count, nullptr, data}); return {}; })); } else if (p.staticMethod != nullptr) { + descriptor.setProperty(rt, "writable", jsi::Value{(p.attributes & napi_writable) != 0}); descriptor.setProperty(rt, "value", jsi::Function::createFromHostFunction(rt, name, 0, [env, method{p.staticMethod}, data{p.data}](jsi::Runtime& rt, const jsi::Value& thisVal, const jsi::Value* args, size_t count) -> jsi::Value { return {rt, (*method)({env, thisVal, args, count, nullptr, data})}; @@ -2344,6 +2346,7 @@ ObjectWrap::DefineClass(napi_env env, descriptor.setProperty(rt, "writable", jsi::Value{(p.attributes & napi_writable) != 0}); descriptor.setProperty(rt, "value", static_cast(p.staticValue)); } else if (p.instanceVoidMethod != nullptr) { + descriptor.setProperty(rt, "writable", jsi::Value{(p.attributes & napi_writable) != 0}); descriptor.setProperty(rt, "value", jsi::Function::createFromHostFunction(rt, name, 0, [env, method{p.instanceVoidMethod}, data{p.data}](jsi::Runtime& rt, const jsi::Value& thisVal, const jsi::Value* args, size_t count) -> jsi::Value { T* nativeObject{Unwrap(env, thisVal.getObject(rt))}; @@ -2351,6 +2354,7 @@ ObjectWrap::DefineClass(napi_env env, return {}; })); } else if (p.instanceMethod != nullptr) { + descriptor.setProperty(rt, "writable", jsi::Value{(p.attributes & napi_writable) != 0}); descriptor.setProperty(rt, "value", jsi::Function::createFromHostFunction(rt, name, 0, [env, method{p.instanceMethod}, data{p.data}](jsi::Runtime& rt, const jsi::Value& thisVal, const jsi::Value* args, size_t count) -> jsi::Value { T* nativeObject{Unwrap(env, thisVal.getObject(rt))}; diff --git a/Polyfills/Blob/Source/Blob.cpp b/Polyfills/Blob/Source/Blob.cpp index 242763fe..dabf6f24 100644 --- a/Polyfills/Blob/Source/Blob.cpp +++ b/Polyfills/Blob/Source/Blob.cpp @@ -3,11 +3,17 @@ #include #include +namespace +{ + constexpr auto JS_BLOB_CONSTRUCTOR_NAME = "Blob"; + // Hidden global holding the polyfill's own constructor (see Babylon::Polyfills::Blob::TryGetData). + constexpr auto JS_BLOB_CONSTRUCTOR_KEY = "__jsRuntimeHostBlob"; +} + namespace Babylon::Polyfills::Internal { void Blob::Initialize(Napi::Env env) { - static constexpr auto JS_BLOB_CONSTRUCTOR_NAME = "Blob"; if (env.Global().Get(JS_BLOB_CONSTRUCTOR_NAME).IsUndefined()) { Napi::Function func = DefineClass( @@ -21,6 +27,15 @@ namespace Babylon::Polyfills::Internal InstanceMethod("bytes", &Blob::Bytes), }); + // TryGetData identifies our instances through this hidden, non-writable, non-configurable + // global, so a script that later replaces the global `Blob` (a test shim, another + // polyfill) cannot make a foreign object look like one of ours. A plain global rather + // than the JsRuntime native object: worker environments have no JsRuntime. + auto constructorDescriptor = Napi::Object::New(env); + constructorDescriptor.Set("value", func); + env.Global().Get("Object").As().Get("defineProperty").As().Call( + env.Global().Get("Object"), + {env.Global(), Napi::String::New(env, JS_BLOB_CONSTRUCTOR_KEY), constructorDescriptor}); env.Global().Set(JS_BLOB_CONSTRUCTOR_NAME, func); } } @@ -156,7 +171,9 @@ namespace Babylon::Polyfills::Blob // This keeps the check portable across QuickJS, V8, JavaScriptCore, Chakra and JSI, and it // also accepts Blob subclasses (e.g. File). We deliberately avoid napi_instanceof, whose // node-addon-api wrapper requires a Napi::Function. - const auto blobConstructor = global.Get("Blob"); + // Our constructor is the one Initialize pinned under the hidden key, not the global `Blob` + // binding: a script may have replaced that with its own class whose instances wrap nothing. + const auto blobConstructor = global.Get(JS_BLOB_CONSTRUCTOR_KEY); if (!blobConstructor.IsFunction()) { return std::nullopt; @@ -200,6 +217,10 @@ namespace Babylon::Polyfills::Blob } const auto* blob = Internal::Blob::Unwrap(object); + if (blob == nullptr) + { + return std::nullopt; + } return BlobData{blob->Data(), blob->Type()}; } } diff --git a/Polyfills/URL/Source/URL.cpp b/Polyfills/URL/Source/URL.cpp index 3d2b9d7d..eb61eb95 100644 --- a/Polyfills/URL/Source/URL.cpp +++ b/Polyfills/URL/Source/URL.cpp @@ -456,6 +456,11 @@ namespace namespace Babylon::Polyfills::Internal { + namespace + { + constexpr auto WebIdlOperationAttributes = static_cast(napi_writable | napi_enumerable | napi_configurable); + } + static constexpr auto JS_URL_CONSTRUCTOR_NAME = "URL"; void URL::Initialize(Napi::Env env) @@ -485,10 +490,13 @@ namespace Babylon::Polyfills::Internal InstanceMethod("toString", &URL::ToString), InstanceMethod("toJSON", &URL::ToJSON), // Static methods - StaticMethod("canParse", &URL::CanParse), - StaticMethod("parse", &URL::Parse), - StaticMethod("createObjectURL", &URL::CreateObjectURL), - StaticMethod("revokeObjectURL", &URL::RevokeObjectURL), + // WebIDL static operations are writable, enumerable and configurable (a page can + // replace URL.createObjectURL); napi_default would make them read-only and let + // such an assignment fail silently. + StaticMethod("canParse", &URL::CanParse, WebIdlOperationAttributes), + StaticMethod("parse", &URL::Parse, WebIdlOperationAttributes), + StaticMethod("createObjectURL", &URL::CreateObjectURL, WebIdlOperationAttributes), + StaticMethod("revokeObjectURL", &URL::RevokeObjectURL, WebIdlOperationAttributes), }); env.Global().Set(JS_URL_CONSTRUCTOR_NAME, func); diff --git a/Tests/UnitTests/Source/Scripts/tests.url.ts b/Tests/UnitTests/Source/Scripts/tests.url.ts index edc72ad5..5e50c103 100644 --- a/Tests/UnitTests/Source/Scripts/tests.url.ts +++ b/Tests/UnitTests/Source/Scripts/tests.url.ts @@ -442,6 +442,43 @@ describe("URL.createObjectURL", function () { expect(() => URL.createObjectURL("not a blob" as any)).to.throw(); }); + it("identifies real Blobs, and rejects look-alikes, after a script replaces the global Blob", function () { + // A page or test harness may install its own Blob class. Identity must come from the + // polyfill's own constructor: a real Blob still works and a foreign instance is a clean + // TypeError, not a bare "Invalid argument" from unwrapping an object that wraps nothing. + // ChakraCore has no globalThis; the sloppy-mode Function trick reaches the global object everywhere. + const globalObject: any = Function("return this")(); + const NativeBlob = Blob; + const real = new NativeBlob(["hello"], { type: "text/plain" }); + class LookAlikeBlob { + size = 5; + type = "text/plain"; + } + globalObject.Blob = LookAlikeBlob; + try { + const url = URL.createObjectURL(real); + expect(url.indexOf("blob:")).to.equal(0); + URL.revokeObjectURL(url); + // Message only: the JSI adapter still surfaces native throws as plain Errors. + expect(() => URL.createObjectURL(new LookAlikeBlob() as any)).to.throw(/not a Blob/); + } finally { + globalObject.Blob = NativeBlob; + } + }); + + it("exposes createObjectURL as a writable, configurable static (WebIDL operation)", function () { + const descriptor = Object.getOwnPropertyDescriptor(URL, "createObjectURL")!; + expect(descriptor.writable, "writable").to.equal(true); + expect(descriptor.configurable, "configurable").to.equal(true); + const original = URL.createObjectURL; + try { + (URL as any).createObjectURL = () => "blob:replaced"; + expect(URL.createObjectURL(new Blob([]))).to.equal("blob:replaced"); + } finally { + (URL as any).createObjectURL = original; + } + }); + it("resolves a blob: URL through fetch (text + content-type)", async function () { const url = URL.createObjectURL(new Blob(["hello blob"], { type: "text/plain" })); const response = await fetch(url);