Skip to content

Linker always uses the latest security schema: img|src/video|src sanitizers dropped for Angular < 22.2.2 #586

Closed

Description

@edbzn

Summary

Since #577 (0.0.41), the compiler matches Angular 22.2.2+, which removed img|src and video|src
from the URL security context (angular/angular@439ff1bdf4). That's gated on angularVersion, but:

  1. The linker can't be told the version. linkAngularPackage(code, filename) takes no
    options, and angular-linker-plugin.js calls it without the plugin's angularVersion. Every
    partially compiled library in node_modules is linked with the 22.2.2+ schema, whatever
    Angular the app runs.
  2. The default is "latest". With angularVersion unset (the README examples omit it), app
    code is also compiled with the 22.2.2+ schema even when the installed @angular/core is older.

The emit change only works with a matching runtime. angular/angular@439ff1bdf4 also added
unwrapImgVideoSrcValue() to setDomProperty/elementAttributeInternal in @angular/core. That
shim unwraps SafeValues and maps null/undefined to '' for img/video src, because the
sanitizer used to do both. On @angular/core ≤ 22.2.1 the shim doesn't exist. Code emitted for
22.2.2+ then assigns a SafeUrl from DomSanitizer.bypassSecurityTrustUrl() as the raw
SafeValue object (and null as "null"). The image fails to load, (load) never fires, and
nothing reports an error. Untrusted URLs also skip the sanitization ngc for that version applies.

Reproduction (@oxc-angular/vite@0.0.41, installed @angular/core@22.2.0)

Linker:

import { linkAngularPackage } from '@oxc-angular/vite/api';
const code = `import * as i0 from '@angular/core';
class Cmp {
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.2.0", type: Cmp,
    isStandalone: true, selector: "x-cmp", ngImport: i0,
    template: '<img [src]="url"><video [src]="url"></video><a [href]="url"></a>' });
}
export { Cmp };`;
console.log((await linkAngularPackage(code, '/node_modules/x-lib/fesm2022/x-lib.mjs')).code);

Actual:

i0.ɵɵproperty("src", ctx.url);                      // img: no sanitizer
i0.ɵɵproperty("src", ctx.url);                      // video: no sanitizer
i0.ɵɵproperty("href", ctx.url, i0.ɵɵsanitizeUrl);

Expected for an app on Angular 22.2.0: i0.ɵɵproperty("src", ctx.url, i0.ɵɵsanitizeUrl) for
img and video, as ngc/@angular/compiler-cli 22.2.0 emits.

Compiler, for comparison:

transformAngularFile(src, id, {}, res)                                         // ɵɵproperty("src", ctx.u)
transformAngularFile(src, id, { angularVersion: { major: 22, minor: 2, patch: 0 } }, res)
                                                                                // ɵɵproperty("src", ctx.u, i0.ɵɵsanitizeUrl)

Real-world impact

ngx-image-cropper 9.1.7 binds [src]="safeImgDataUrl()" (a SafeUrl). Under OXC, the cropper
never loads its image and imageCropped never emits. That failed 4 component tests in our
monorepo: the cropper and an upload-photo dialog built on it. Setting angularVersion doesn't
help, because the library goes through the linker.

Suggested fix

  • Thread angularVersion into the linker (linkAngularPackage(code, filename, { angularVersion }))
    and pass it from angular-linker-plugin, including the optimizeDeps rolldown plugin path.
  • When angularVersion is unset, detect the installed @angular/core version (resolve
    @angular/core/package.json from the project root) instead of assuming latest, so a minor
    bump of OXC can't silently change emitted security behavior.

Environment

  • @oxc-angular/vite 0.0.41 (linux-x64-gnu binding)
  • Angular 22.2.0, Vite 7.3.6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions