Skip to content

fix: security hardening - #36

Merged
triniwiz merged 6 commits into
mainfrom
fix/security-hardening
Oct 30, 2026
Merged

triniwiz merged 6 commits into
mainfrom
fix/security-hardening

Conversation

@triniwiz

Copy link
Copy Markdown
Member

A security pass before the first major release. It makes the runtime refuse JS data that would otherwise become native pointers or mistyped COM objects, keeps app console output out of persistent logs in release builds, and tightens the debug inspector, remote module loading, source protection and the release pipeline. Each commit stands on its own; they are listed in order below.

Changes

ci: build published variants from source and pin the napi workflow. Tag releases of the engine variants now rebuild the shared tools and DotNetBridge instead of shipping the copies committed under template/framework, and fail if a tool is missing. windows-napi.yml gets a read-only default token, actions pinned to the SHAs npm_release.yml uses, and npm provenance.

fix(devtools): refuse inspector connections from web pages. The inspector listens on a fixed loopback path during ns debug, and browsers let any page open a WebSocket to localhost. The handshake now requires a devtools:// Origin or none (VS Code and other CDP clients send none), and a non-loopback Host is rejected to stop DNS rebinding.

fix(quickjs): honor view offsets and embedded NULs in the napi shim. Typed-array and DataView info returned the buffer's base pointer instead of the view's first byte, and UTF-16 strings and evaluated scripts stopped at an embedded NUL.

feat(source-protect): seal app.nsbundle with a per-build key. Bundles were sealed with one default key compiled into the public runtime. nsbundle_pack --write-key now seals with a fresh random key that the template embeds and RuntimeHost passes to runtime_set_bundle_key; the CLI side is NativeScript/nativescript-cli@8756c1cb1. A corrupt bundle now fails to load instead of aborting the app.

chore(template): drop privateNetworkClientServer. It gates nothing in a full-trust app and only adds a Store disclosure. internetClient stays.

fix(runtime): harden marshaling, release logging and remote loading. On both the classic and napi engines:

  • Native pointers come only from an External, a BigInt or null. A Number or string handle / __native_ptr, which JSON.parse can produce, is refused.
  • Interface and class parameters QueryInterface every value, boxed primitives included, with no raw-pointer fallback. Delegate parameters accept runtime-made JS delegates and QueryInterface anything else, and GenericMethodCall no longer calls a slot on the wrong interface.
  • Call shapes the libffi layout can't express (non-byte arrays, received arrays, array returns, structs over the 128-byte return buffer, JS delegates taking arrays) fail when the call is built instead of corrupting it.
  • Struct buffers must cover the struct, and serialized structs are padded to their ABI size.
  • Buffer arguments are rechecked before the call, in case JS detached or resized them while later arguments were parsed.
  • URL objects carry a type tag, so they're never read as WinRT wrappers (instanceof, argument passing) or as each other.
  • Release builds no longer write console output to the Windows Event Log or the console.log trace file.
  • Remote modules: allowlist entries match by origin and path boundary, the final URL after redirects must pass the gate too, and NSWinRT.dynamicImport goes through the native loader.
  • WebSocket subprotocols must be HTTP tokens, and one message is capped at 100 MiB.
  • New crypto.getRandomValues / crypto.randomUUID on the OS RNG.
  • NSWinRT.win32.bind loads a bare DLL name from the app directory and System32 only.

Behavior changes

  • A wrong-typed argument, a numeric handle, or an unsupported array shape now throws a TypeError where it used to crash or misbehave.
  • remoteModuleAllowlist entries match whole path segments: https://cdn.example.com/lib no longer allows /library.js.
  • NSWinRT.dynamicImport reads the top-level security key from the app's package.json (as the module loader does), not nativescript.security.
  • win32.bind('foo.dll') no longer finds a DLL that is only on PATH; pass a full path.
  • Release builds keep no console.log file; output still goes to OutputDebugString and stderr.

Testing

  • cargo test -p runtime: 163 passed; the 11 failures are the environment-only baseline (UI thread, dispatcher, ApplicationData). New tests cover each rule above; on main, the JSON-handle and wrong-type-argument tests crash with an access violation.
  • windows-runtime-integration-tests and runtime-devtools: all pass.
  • windows-napi npm test: 439 passed, 0 failed.
  • packages/test/{interop,enum,security}-check.js on QuickJS, Hermes, V8 and JSC: all pass.
  • A Vite + React app (shadcn starter) on the patched classic runtime: renders, navigates and toggles theme with a clean log.
  • Not yet run: a full ns build windows --release with source protection, and the CI changes, which take effect on the next tagged release.

Engine-variant releases staged the tool binaries and DotNetBridge committed under
template/framework unless a manual run set rebuild_framework, so a tag release published
binaries built off CI under a CI provenance attestation. Tag releases now always rebuild the
shared framework, clear the committed tool binaries first and fail if a tool is missing.

windows-napi.yml gets a read-only default token, actions pinned to the SHAs npm_release.yml
already uses, and npm provenance for the root and per-triple packages.
Any page in the developer's browser could open a WebSocket to the fixed
ws://127.0.0.1:43000/devtools/page/runtime path during `ns debug` and run arbitrary code
through the inspector. The handshake now requires a devtools:// Origin or none (VS Code and
other CDP clients send none), and both the handshake and /json reject a non-loopback Host,
which defeats DNS rebinding.
napi_get_typedarray_info and napi_get_dataview_info returned the ArrayBuffer's base pointer
instead of the view's first byte, so native code read and wrote the wrong bytes of a
subarray. napi_get_value_string_utf16 and script evaluation used strlen, cutting strings at
an embedded NUL.
Without --key-hex every bundle was sealed with the one default key compiled into the public
runtime DLL, so a key extracted once opened any app's bundle. `nsbundle_pack --write-key
<file>` seals with a fresh random key and writes it beside the project; the template embeds it
as the NSBundleKey resource and RuntimeHost passes it to runtime_set_bundle_key before
runtime_init. The CLI passes --write-key when no key is given.

The container parser also bounds a corrupt entry count's allocation and checks blob offsets
without overflow, so a damaged bundle fails to load instead of aborting the app.
A runFullTrust app doesn't run in an AppContainer, so the capability gated nothing and only
added a "home or work networks" disclosure to the Store listing. internetClient stays; an app
or plugin that wants the capability can add it through the manifest merger.
Marshaling (both the classic and napi engines):
- A pointer comes only from an External, a BigInt or null. A Number or string `handle` or
  `__native_ptr` is refused, since JSON can carry those: `{"handle": 1094795585}` passed to
  an Object parameter used to be AddRef'd as a COM pointer.
- Interface and class parameters QueryInterface every pointer, boxed primitives included, and
  no longer fall back to the raw pointer when that fails, so a string passed as a Uri can't be
  called through the wrong vtable. Delegate parameters accept JS delegates the runtime made and
  QueryInterface anything else; GenericMethodCall fails instead of calling a slot on the wrong
  interface.
- Shapes the call layout can't express (non-byte arrays, received arrays, array returns,
  structs larger than the return buffer, JS delegates taking arrays) fail at construction
  instead of corrupting the libffi call.
- Struct buffers must cover the struct, serialized structs are padded to their ABI size, and
  IReference<T> struct fields are pointer-sized.
- Buffer arguments are rechecked before the call, since parsing a later argument can run JS
  that detaches or resizes them.
- Only two-field wrappers are read as a DeclarationFFI, and URL accessors check their receiver,
  so a URL passed to WinRT or `instanceof` is no longer read as the wrong type.

Release builds keep app console output out of the Windows Event Log and the on-disk trace log,
and the trace log no longer falls back to a hardcoded user directory.

Remote modules: allowlist entries match by origin and path boundary rather than string
prefix, the response's final URL must also pass the gate after redirects, the gate reads the
entry's package.json for script entries too, and NSWinRT.dynamicImport goes through the native
loader. WebSocket subprotocols must be HTTP tokens and a message is capped at 100 MiB.

Also: crypto.getRandomValues and crypto.randomUUID backed by the OS RNG, and NSWinRT.win32.bind
loads a bare DLL name from the app directory and System32 only.
@coderabbitai

coderabbitai Bot commented Oct 30, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 1ad10f1f-fe19-44f3-8405-e89c3e6f20f7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@triniwiz
triniwiz merged commit b405b15 into main Oct 30, 2026
4 checks passed
@triniwiz triniwiz changed the title fix: security hardening before the first major release fix: security hardening Oct 30, 2026
@triniwiz
triniwiz deleted the fix/security-hardening branch October 30, 2026 03:36
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