From a17de3ad5e89a3e4973324da055c123ef68c8142 Mon Sep 17 00:00:00 2001 From: Thodoris Sotiropoulos Date: Wed, 7 Oct 2026 07:08:10 +0000 Subject: [PATCH] Check a type tag instead of instanceof before unwrapping a native object --- CHANGELOG.md | 1 + src/Canvas.cc | 4 +++- src/CanvasGradient.cc | 1 + src/CanvasPattern.cc | 5 +++-- src/CanvasRenderingContext2d.cc | 17 +++++++++-------- src/FontFace.cc | 2 ++ src/FontFaceSet.cc | 7 ++++--- src/Image.cc | 12 +++++++++++- src/ImageData.cc | 1 + src/InstanceData.h | 24 ++++++++++++++++++++++++ 10 files changed, 59 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26fb99743..c72d5dfe4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed +* Check a type tag before unwrapping a native object, so a forged prototype chain can no longer crash the process (#2628) * Load images from Node.js object URLs (#2525) 3.2.3 diff --git a/src/Canvas.cc b/src/Canvas.cc index da8e8fb8f..75c1175c4 100644 --- a/src/Canvas.cc +++ b/src/Canvas.cc @@ -62,6 +62,7 @@ Canvas::Initialize(Napi::Env& env, Napi::Object& exports) { */ Canvas::Canvas(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info), env(info.Env()) { + info.This().As().TypeTag(&info.Env().GetInstanceData()->tags.Canvas); InstanceData* data = env.GetInstanceData(); ctor = Napi::Persistent(data->CanvasCtor.Value()); @@ -722,7 +723,8 @@ Canvas::resurface(Napi::Object This, uint16_t width, uint16_t height) { this->width = width; this->height = height; ensureSurface(); - if (This.Get("context").UnwrapTo(&context) && context.IsObject()) { + if (This.Get("context").UnwrapTo(&context) && context.IsObject() + && context.As().CheckTypeTag(&env.GetInstanceData()->tags.Context2d)) { // Reset context Context2d *context2d = Context2d::Unwrap(context.As()); cairo_t *prev = context2d->context(); diff --git a/src/CanvasGradient.cc b/src/CanvasGradient.cc index 348dc7703..94c8f3d53 100644 --- a/src/CanvasGradient.cc +++ b/src/CanvasGradient.cc @@ -27,6 +27,7 @@ Gradient::Initialize(Napi::Env& env, Napi::Object& exports) { */ Gradient::Gradient(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info), env(info.Env()) { + info.This().As().TypeTag(&info.Env().GetInstanceData()->tags.Gradient); // Linear if ( 4 == info.Length() && diff --git a/src/CanvasPattern.cc b/src/CanvasPattern.cc index 9ad5b83e6..49f30e522 100644 --- a/src/CanvasPattern.cc +++ b/src/CanvasPattern.cc @@ -29,6 +29,7 @@ Pattern::Initialize(Napi::Env& env, Napi::Object& exports) { */ Pattern::Pattern(const Napi::CallbackInfo& info) : ObjectWrap(info), env(info.Env()) { + info.This().As().TypeTag(&info.Env().GetInstanceData()->tags.Pattern); if (!info[0].IsObject()) { Napi::TypeError::New(env, "Image or Canvas expected").ThrowAsJavaScriptException(); return; @@ -38,7 +39,7 @@ Pattern::Pattern(const Napi::CallbackInfo& info) : ObjectWrap(info), en InstanceData* data = env.GetInstanceData(); // Image - if (obj.InstanceOf(data->ImageCtor.Value()).UnwrapOr(false)) { + if (obj.CheckTypeTag(&data->tags.Image)) { image = Image::Unwrap(obj); if (!image->isComplete()) { Napi::Error::New(env, "Image given has not completed loading").ThrowAsJavaScriptException(); @@ -47,7 +48,7 @@ Pattern::Pattern(const Napi::CallbackInfo& info) : ObjectWrap(info), en value = Napi::Persistent(obj); // Canvas - } else if (obj.InstanceOf(data->CanvasCtor.Value()).UnwrapOr(false)) { + } else if (obj.CheckTypeTag(&data->tags.Canvas)) { canvas = Canvas::Unwrap(obj); value = Napi::Persistent(obj); // Invalid diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 6183837c0..fe580226a 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -167,6 +167,7 @@ Context2d::Initialize(Napi::Env& env, Napi::Object& exports) { */ Context2d::Context2d(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info), env(info.Env()) { + info.This().As().TypeTag(&info.Env().GetInstanceData()->tags.Context2d); InstanceData* data = env.GetInstanceData(); if (!info[0].IsObject()) { @@ -175,7 +176,7 @@ Context2d::Context2d(const Napi::CallbackInfo& info) : Napi::ObjectWrap(); - if (!obj.InstanceOf(data->CanvasCtor.Value()).UnwrapOr(false)) { + if (!obj.CheckTypeTag(&data->tags.Canvas)) { if (!env.IsExceptionPending()) { Napi::TypeError::New(env, "Canvas expected").ThrowAsJavaScriptException(); } @@ -761,7 +762,7 @@ Context2d::PutImageData(const Napi::CallbackInfo& info) { } Napi::Object obj = info[0].As(); InstanceData* data = env.GetInstanceData(); - if (!obj.InstanceOf(data->ImageDataCtor.Value()).UnwrapOr(false)) { + if (!obj.CheckTypeTag(&data->tags.ImageData)) { if (!env.IsExceptionPending()) { Napi::TypeError::New(env, "ImageData expected").ThrowAsJavaScriptException(); } @@ -1259,7 +1260,7 @@ Context2d::DrawImage(const Napi::CallbackInfo& info) { Napi::Object obj = info[0].As(); // Image - if (obj.InstanceOf(env.GetInstanceData()->ImageCtor.Value()).UnwrapOr(false)) { + if (obj.CheckTypeTag(&env.GetInstanceData()->tags.Image)) { Image *img = Image::Unwrap(obj); if (!img->isComplete()) { Napi::Error::New(env, "Image given has not completed loading").ThrowAsJavaScriptException(); @@ -1270,7 +1271,7 @@ Context2d::DrawImage(const Napi::CallbackInfo& info) { surface = img->surface.surface(); // Canvas - } else if (obj.InstanceOf(env.GetInstanceData()->CanvasCtor.Value()).UnwrapOr(false)) { + } else if (obj.CheckTypeTag(&env.GetInstanceData()->tags.Canvas)) { Canvas *canvas = Canvas::Unwrap(obj); source_w = sw = canvas->getWidth(); source_h = sh = canvas->getHeight(); @@ -1903,9 +1904,9 @@ Context2d::SetFillStyle(const Napi::CallbackInfo& info, const Napi::Value& value } else if (value.IsObject()) { InstanceData *data = env.GetInstanceData(); Napi::Object obj = value.As(); - if (obj.InstanceOf(data->CanvasGradientCtor.Value()).UnwrapOr(false)) { + if (obj.CheckTypeTag(&data->tags.Gradient)) { state->fillGradient.Reset(obj); - } else if (obj.InstanceOf(data->CanvasPatternCtor.Value()).UnwrapOr(false)) { + } else if (obj.CheckTypeTag(&data->tags.Pattern)) { state->fillPattern.Reset(obj); } } @@ -1940,9 +1941,9 @@ Context2d::SetStrokeStyle(const Napi::CallbackInfo& info, const Napi::Value& val } else if (value.IsObject()) { InstanceData *data = env.GetInstanceData(); Napi::Object obj = value.As(); - if (obj.InstanceOf(data->CanvasGradientCtor.Value()).UnwrapOr(false)) { + if (obj.CheckTypeTag(&data->tags.Gradient)) { state->strokeGradient.Reset(obj); - } else if (obj.InstanceOf(data->CanvasPatternCtor.Value()).UnwrapOr(false)) { + } else if (obj.CheckTypeTag(&data->tags.Pattern)) { state->strokePattern.Reset(obj); } } diff --git a/src/FontFace.cc b/src/FontFace.cc index 3b8449451..80b8e5d17 100644 --- a/src/FontFace.cc +++ b/src/FontFace.cc @@ -14,6 +14,8 @@ FontFace::FontFace(const Napi::CallbackInfo& info) : id(++lastId), env(info.Env()) { + info.This().As().TypeTag(&env.GetInstanceData()->tags.FontFace); + if (info.Length() < 2) { Napi::TypeError::New(env, "Family and source arguments are required").ThrowAsJavaScriptException(); return; diff --git a/src/FontFaceSet.cc b/src/FontFaceSet.cc index 4c06e70c4..7810ec4b7 100644 --- a/src/FontFaceSet.cc +++ b/src/FontFaceSet.cc @@ -9,6 +9,7 @@ FontFaceSet::FontFaceSet(Napi::CallbackInfo& info) : env(info.Env()), ready(Napi::Promise::Deferred::New(info.Env())), Napi::ObjectWrap(info) { + info.This().As().TypeTag(&env.GetInstanceData()->tags.FontFaceSet); } void @@ -52,7 +53,7 @@ FontFaceSet::Add(const Napi::CallbackInfo& info) { isFontFace = false; } else { obj = info[0].As(); - if (!obj.InstanceOf(data->FontFaceCtor.Value()).UnwrapTo(&isFontFace)) return env.Undefined(); + isFontFace = obj.CheckTypeTag(&data->tags.FontFace); } if (!isFontFace) { Napi::TypeError::New(env, "Expected instance of FontFace").ThrowAsJavaScriptException(); @@ -85,7 +86,7 @@ FontFaceSet::Has(const Napi::CallbackInfo& info) { isFontFace = false; } else { obj = info[0].As(); - if (!obj.InstanceOf(data->FontFaceCtor.Value()).UnwrapTo(&isFontFace)) return env.Undefined(); + isFontFace = obj.CheckTypeTag(&data->tags.FontFace); } if (!isFontFace) { Napi::TypeError::New(env, "Expected instance of FontFace").ThrowAsJavaScriptException(); @@ -124,7 +125,7 @@ FontFaceSet::Delete(const Napi::CallbackInfo& info) { isFontFace = false; } else { obj = info[0].As(); - if (!obj.InstanceOf(data->FontFaceCtor.Value()).UnwrapTo(&isFontFace)) return env.Undefined(); + isFontFace = obj.CheckTypeTag(&data->tags.FontFace); } if (!isFontFace) { Napi::TypeError::New(env, "Expected instance of FontFace").ThrowAsJavaScriptException(); diff --git a/src/Image.cc b/src/Image.cc index 2639989b5..6a9375118 100644 --- a/src/Image.cc +++ b/src/Image.cc @@ -1488,6 +1488,7 @@ Image::Initialize(Napi::Env& env, Napi::Object& exports) { */ Image::Image(const Napi::CallbackInfo& info) : ObjectWrap(info), env(info.Env()) , surface(env) { + info.This().As().TypeTag(&info.Env().GetInstanceData()->tags.Image); info.This().ToObject().Unwrap().Set("onload", env.Null()); info.This().ToObject().Unwrap().Set("onerror", env.Null()); } @@ -1586,7 +1587,12 @@ Image::SetHeight(const Napi::CallbackInfo& info, const Napi::Value& value) { Napi::Value Image::GetSource(const Napi::CallbackInfo& info){ Napi::Env env = info.Env(); - Image *img = Image::Unwrap(info.This().As()); + Napi::Object This = info.This().As(); + if (!This.CheckTypeTag(&env.GetInstanceData()->tags.Image)) { + Napi::TypeError::New(env, "Image expected").ThrowAsJavaScriptException(); + return env.Undefined(); + } + Image *img = Image::Unwrap(This); return Napi::String::New(env, img->surface.filename ? img->surface.filename : ""); } @@ -1598,6 +1604,10 @@ void Image::SetSource(const Napi::CallbackInfo& info){ Napi::Env env = info.Env(); Napi::Object This = info.This().As(); + if (!This.CheckTypeTag(&env.GetInstanceData()->tags.Image)) { + Napi::TypeError::New(env, "Image expected").ThrowAsJavaScriptException(); + return; + } Image *img = Image::Unwrap(This); cairo_status_t status = CAIRO_STATUS_READ_ERROR; diff --git a/src/ImageData.cc b/src/ImageData.cc index 14f44297e..c0ef6c32a 100644 --- a/src/ImageData.cc +++ b/src/ImageData.cc @@ -25,6 +25,7 @@ ImageData::Initialize(Napi::Env& env, Napi::Object& exports) { */ ImageData::ImageData(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info), env(info.Env()) { + info.This().As().TypeTag(&info.Env().GetInstanceData()->tags.ImageData); Napi::TypedArray dataArray; uint32_t width; uint32_t height; diff --git a/src/InstanceData.h b/src/InstanceData.h index b8844ffad..398dab9be 100644 --- a/src/InstanceData.h +++ b/src/InstanceData.h @@ -1,6 +1,8 @@ #pragma once #include +#include +#include #include #include "FontFaceSet.h" #ifdef _WIN32 @@ -15,7 +17,29 @@ using PlatformFontManager = FontManagerMacos; using PlatformFontManager = FontManagerLinux; #endif +// Per-class type tags; applied on construction and checked before napi_unwrap. +struct TypeTags { + napi_type_tag Canvas; + napi_type_tag Context2d; + napi_type_tag Image; + napi_type_tag ImageData; + napi_type_tag Gradient; + napi_type_tag Pattern; + napi_type_tag FontFace; + napi_type_tag FontFaceSet; + + TypeTags() { + std::random_device rd; + for (napi_type_tag* tag : {&Canvas, &Context2d, &Image, &ImageData, + &Gradient, &Pattern, &FontFace, &FontFaceSet}) { + tag->lower = (static_cast(rd()) << 32) | rd(); + tag->upper = (static_cast(rd()) << 32) | rd(); + } + } +}; + struct InstanceData { + TypeTags tags; Napi::FunctionReference CanvasCtor; Napi::FunctionReference CanvasGradientCtor; Napi::FunctionReference DOMMatrixCtor;