Compare commits

...

23 Commits

Author SHA1 Message Date
leonsenft 4c0bc4345a release: cut the v21.2.22 release 2026-08-26 16:51:57 -07:00
Alan Agius 5aa6d97deb fix(platform-server): avoid stripping unicode whitespace during url resolution
Avoid trimming urlStr with String.prototype.trim() in resolveUrl to ensure URL parsing and resolution align with the WHATWG URL standard.
2026-08-25 10:00:02 -07:00
Alan Agius 73d8bbd27c fix(platform-server): update domino to latest version
Updates the domino dependency to the latest version as used in the main branch.
2026-08-24 10:41:12 -07:00
Jessica Janiuk 992d627b69 release: cut the v21.2.21 release 2026-08-19 12:52:45 -07:00
SkyZeroZx c19a36c2fb fix(platform-browser): disallow event handler attributes in Meta
Prevent arbitrary MetaDefinition properties from writing on* handlers directly to meta elements. Browser events can execute these handlers, including on meta elements rendered in the document body.

Reject handler attributes case-insensitively before matching or mutating tags.

Fixes #70126
2026-08-17 13:43:57 -07:00
Alon Mishne dca0a77488 release: cut the v21.2.20 release 2026-08-12 17:02:31 -07:00
Jaime Burgos caf616670f fix(http): run root interceptors in the terminal request chain
Represent withRequestsMadeViaParent() with an internal delegating backend so the interceptor handler can distinguish delegated clients from independent child configurations.
2026-07-31 15:13:30 -07:00
SkyZeroZx fec5977df4 fix(http): match header values exactly when deleting
Normalize value-specific HttpHeaders deletions before filtering. The string overload previously used String#indexOf and removed shorter values contained within the requested deletion value, potentially widening outgoing request metadata.

Preserve delete-all behavior only when no value is supplied, and cover string, array, and empty-string deletion.
2026-07-31 15:13:30 -07:00
SkyZeroZx e33d69a71c fix(http): preserve immutability of materialized clones
Prevent lazy HttpHeaders and HttpParams clones from reusing value arrays owned by a materialized source. Append and value-specific delete operations previously mutated those shared arrays, violating the immutable API contract and allowing request metadata to bleed into later requests.

Share value arrays until an update mutates a specific header or parameter, then copy only that array. Cover the affected append and delete paths with regression tests that materialize the source first.
2026-07-31 15:13:30 -07:00
Jaime Burgos 6afe6fa781 fix(core): sanitize host bindings on concrete hosts
Compute host binding security contexts against concrete hosts, including host directives, inheritance, dynamic directives, and createComponent hostElement usage.
2026-07-30 08:29:04 -07:00
Alan Agius 07cb840eac release: cut the v21.2.19 release 2026-07-29 22:44:48 +02:00
Alan Agius 7b884f585a fix(compiler): restrict possible event handler check to property names longer than 2 characters
Previously, the compiler disallowed translation of any attribute starting with 'on' for security reasons. This incorrectly disallowed translation of the 'on' attribute itself, which is not an event handler.

This commit introduces `isPossibleEventHandler` to verify that the property name has a length greater than 2 in addition to starting with 'on'. This allows attributes like 'on' to be translated while still correctly disallowing actual event handlers like 'onerror', 'onclick', etc.
2026-07-29 08:51:50 -07:00
Hexix23 948a8d6831 fix(http): distinguish repeated transfer cache params
Serialize transfer cache request parameters without comma-joining repeated values so distinct HttpClient requests cannot reuse the same cached response.
2026-07-29 08:51:50 -07:00
Hexix23 e2660c3dee fix(compiler): disallow i18n event attributes
Reject translated event-handler attributes so localization cannot bypass Angular event-attribute validation.
2026-07-29 08:51:50 -07:00
SkyZeroZx f34a93c946 fix(platform-server): update domino to latest version
Updates the domino dependency to the latest version as used in the main branch.

This update contains fixes for https://github.com/angular/domino/pull/32.
2026-07-29 08:43:40 -07:00
Jaime Burgos 9949dccce1 fix(http): enable xsrf for root-provided HttpClient
Include the XSRF interceptor in the root token factory so the automatically provided HttpClient retains the documented default protection without requiring provideHttpClient().
2026-07-24 13:24:13 -07:00
leonsenft 5d00aa2bd4 release: cut the v21.2.18 release 2026-07-08 15:02:15 -07:00
SkyZeroZx e86c31bf26 fix(service-worker): preserve referrer policy in asset requests
Preserve explicit referrer policy when the service worker reconstructs asset requests for cache-busted and redirected asset fetches.

For example, an application can load a script or image with referrerPolicy: 'same-origin' or 'origin' to limit referrer data. Dropping that policy can expose more of the current URL to that resource host.

(cherry picked from commit a7f52e5c30)
2026-07-08 11:24:32 -07:00
SkyZeroZx 1804f73bec fix(service-worker): preserve referrer in asset requests
Preserve referrer metadata when the service worker reconstructs asset requests for cache-busted and redirected asset fetches.

For example, an attacker with access to asset host logs could receive a reset token embedded in a page URL if the reconstructed request falls back to default referrer behavior instead of carrying referrer: ''.

(cherry picked from commit 99ad47e58f)
2026-07-08 11:24:32 -07:00
SkyZeroZx 91df739b80 fix(http): prevent caching of responses with Set-Cookie headers
Skip HttpTransferCache serialization for HTTP responses that contain a
Set-Cookie header.

Cookie-setting responses commonly represent session-specific,
user-specific, or security-sensitive state. Serializing their bodies into
SSR TransferState can embed sensitive data into the generated HTML, where
it may be reused during hydration or replayed by a shared cache/CDN.

(cherry picked from commit 80795defc6)
2026-07-07 10:15:02 -07:00
Alan Agius 8d22cc953b fix(compiler-cli): update babel dependencies to latest v7
Update babel dependencies to v7.29.7 to address CVE-2026-49356.

Fixes #69608
2026-07-06 14:01:46 -07:00
SkyZeroZx 5a693bafcd fix(core): reject dynamic script host elements
The previous fix for GHSA-692r-grfm-v8x7 was incomplete because it rejected script tags only when locating an explicit host element. Dynamic component instantiation can also infer the host element from the component selector.

Move the script-host rejection to the point where ComponentFactory has resolved the host element for either path, so createComponent rejects script hosts consistently.

(cherry picked from commit 135f3755b4)
2026-06-30 17:42:39 -07:00
SkyZeroZx 6bcce117fb fix(core): avoid caching missing locale data
Only cache locale data loaded from the global locale registry when an actual locale entry is found.

This prevents attacker-controlled missing locale identifiers from being retained indefinitely in SSR when locale lookup falls back to a parent locale or the built-in English locale, avoiding unbounded process memory growth in locale-aware pipes and formatters.

(cherry picked from commit ea8277ae37)
2026-06-24 13:04:05 -04:00
61 changed files with 2092 additions and 361 deletions
+76
View File
@@ -1,3 +1,79 @@
<a name="21.2.22"></a>
# 21.2.22 (2026-08-26)
### platform-server
| Commit | Type | Description |
| -- | -- | -- |
| [5aa6d97deb](https://github.com/angular/angular/commit/5aa6d97deb9ef1de14e23748b7fa74f97d183132) | fix | avoid stripping unicode whitespace during url resolution |
| [73d8bbd27c](https://github.com/angular/angular/commit/73d8bbd27cb46495426d4132975a1355b47ad915) | fix | update domino to latest version |
<!-- CHANGELOG SPLIT MARKER -->
<a name="21.2.21"></a>
# 21.2.21 (2026-08-19)
### platform-browser
| Commit | Type | Description |
| -- | -- | -- |
| [c19a36c2fb](https://github.com/angular/angular/commit/c19a36c2fbc51f41ce974ea1fd87f9f56b3e2444) | fix | disallow event handler attributes in Meta |
<!-- CHANGELOG SPLIT MARKER -->
<a name="21.2.20"></a>
# 21.2.20 (2026-08-12)
### core
| Commit | Type | Description |
| -- | -- | -- |
| [6afe6fa781](https://github.com/angular/angular/commit/6afe6fa781c2f0931f0aedd729b9884a8fe212ee) | fix | sanitize host bindings on concrete hosts |
### http
| Commit | Type | Description |
| -- | -- | -- |
| [fec5977df4](https://github.com/angular/angular/commit/fec5977df4dda3a10d5ce2923e3e06d86ba11ee7) | fix | match header values exactly when deleting |
| [e33d69a71c](https://github.com/angular/angular/commit/e33d69a71c5beb8fe5785b53fd6b37658334e8e0) | fix | preserve immutability of materialized clones |
| [caf616670f](https://github.com/angular/angular/commit/caf616670fd20d528aa69e0131cc17d60f0cc27d) | fix | run root interceptors in the terminal request chain |
<!-- CHANGELOG SPLIT MARKER -->
<a name="21.2.19"></a>
# 21.2.19 (2026-07-29)
### compiler
| Commit | Type | Description |
| -- | -- | -- |
| [e2660c3dee](https://github.com/angular/angular/commit/e2660c3deeccb86471f9c11b7e65ebcc89d84e9e) | fix | disallow i18n event attributes |
| [7b884f585a](https://github.com/angular/angular/commit/7b884f585abf50480c271d234c892c141eb61e2c) | fix | restrict possible event handler check to property names longer than 2 characters |
### http
| Commit | Type | Description |
| -- | -- | -- |
| [948a8d6831](https://github.com/angular/angular/commit/948a8d6831e8920b54663ec79421da95210e0e35) | fix | distinguish repeated transfer cache params |
| [9949dccce1](https://github.com/angular/angular/commit/9949dccce164638597496a6b3881043117dfd1df) | fix | enable xsrf for root-provided HttpClient |
### platform-server
| Commit | Type | Description |
| -- | -- | -- |
| [f34a93c946](https://github.com/angular/angular/commit/f34a93c946c017eff526cfb0c8d17f51e97f01e4) | fix | update domino to latest version |
<!-- CHANGELOG SPLIT MARKER -->
<a name="21.2.18"></a>
# 21.2.18 (2026-07-08)
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [8d22cc953b](https://github.com/angular/angular/commit/8d22cc953bb9f970c6f86fb0f6e4d0665b874bfa) | fix | update babel dependencies to latest v7 |
### core
| Commit | Type | Description |
| -- | -- | -- |
| [6bcce117fb](https://github.com/angular/angular/commit/6bcce117fbc616e4e721f351e9e038202e916522) | fix | avoid caching missing locale data |
| [5a693bafcd](https://github.com/angular/angular/commit/5a693bafcd49ef11ce687bd91a209e1a46652f42) | fix | reject dynamic script host elements |
### http
| Commit | Type | Description |
| -- | -- | -- |
| [91df739b80](https://github.com/angular/angular/commit/91df739b8022e2177bcc7214ed9a4e94098a4831) | fix | prevent caching of responses with Set-Cookie headers |
### service-worker
| Commit | Type | Description |
| -- | -- | -- |
| [1804f73bec](https://github.com/angular/angular/commit/1804f73becf425b5b1ccee9751d6e3fbd10e1f0b) | fix | preserve referrer in asset requests |
| [e86c31bf26](https://github.com/angular/angular/commit/e86c31bf26359aacce814167506dbdae585a7eb8) | fix | preserve referrer policy in asset requests |
<!-- CHANGELOG SPLIT MARKER -->
<a name="21.2.17"></a>
# 21.2.17 (2026-06-10)
## Deprecations
+2 -2
View File
@@ -432,7 +432,7 @@ To configure this, update your `angular.json` file as follows:
You can customize how Angular caches HTTP responses during server‑side rendering (SSR) and reuses them during hydration by configuring `HttpTransferCacheOptions`.
This configuration is provided globally using `withHttpTransferCacheOptions` inside `provideClientHydration()`.
By default, `HttpClient` caches all `HEAD` and `GET` requests which don't contain `Authorization`, `Proxy-Authorization`, or `Cookie` headers and are not sent with `withCredentials` or Fetch API `credentials` modes that can send credentials. Angular also skips transfer cache when a request or response includes `Cache-Control` directives that forbid caching (`no-store`, `no-cache`, or `private`), or when the Fetch API `cache` option is set to `no-store` or `no-cache`. You can override the request filtering settings by using `withHttpTransferCacheOptions` in the hydration configuration.
By default, `HttpClient` caches all `HEAD` and `GET` requests which don't contain `Authorization`, `Proxy-Authorization`, or `Cookie` headers and are not sent with `withCredentials` or Fetch API `credentials` modes that can send credentials. Angular also skips transfer cache when a request or response includes `Cache-Control` directives that forbid caching (`no-store`, `no-cache`, or `private`), or when the Fetch API `cache` option is set to `no-store` or `no-cache`. Responses that carry a `Set-Cookie` header are also skipped. You can override the request filtering settings by using `withHttpTransferCacheOptions` in the hydration configuration.
```ts
import {bootstrapApplication} from '@angular/platform-browser';
@@ -560,7 +560,7 @@ To disable caching for an individual request, you can specify the [`transferCach
httpClient.get('/api/sensitive-data', {transferCache: false});
```
`HttpTransferCache` does not cache requests or responses that explicitly opt out of caching. Angular skips transfer cache entries when a request includes a `Cache-Control` header with `no-store`, `no-cache`, or `private`, or when the request uses the Fetch API `cache` option set to `no-store` or `no-cache`. Responses with `Cache-Control: no-store`, `Cache-Control: no-cache`, or `Cache-Control: private` are also not stored in the transfer cache.
`HttpTransferCache` does not cache requests or responses that explicitly opt out of caching. Angular skips transfer cache entries when a request includes a `Cache-Control` header with `no-store`, `no-cache`, or `private`, or when the request uses the Fetch API `cache` option set to `no-store` or `no-cache`. Responses with `Cache-Control: no-store`, `Cache-Control: no-cache`, or `Cache-Control: private` are also not stored in the transfer cache. Responses that include a `Set-Cookie` header are likewise not stored, as they typically carry user-specific state.
NOTE: If your application uses different HTTP origins to make API calls on the server and on the client, the `HTTP_TRANSFER_CACHE_ORIGIN_MAP` token allows you to establish a mapping between those origins, so that `HttpTransferCache` feature can recognize those requests as the same ones and reuse the data cached on the server during hydration on the client.
@@ -13,6 +13,8 @@ export const enum RuntimeErrorCode {
// (undocumented)
HYDRATION_CONFLICTING_FEATURES = 5001,
// (undocumented)
INVALID_EVENT_ATTRIBUTE = 5203,
// (undocumented)
NO_PLUGIN_FOR_EVENT = 5101,
// (undocumented)
ROOT_NODE_NOT_FOUND = -5104,
+1 -1
View File
@@ -15,7 +15,7 @@
"zone.js": "0.16.0"
},
"devDependencies": {
"@babel/core": "7.29.0",
"@babel/core": "7.29.7",
"@rollup/plugin-babel": "^6.0.0",
"@rollup/plugin-node-resolve": "^16.0.0",
"@types/jasmine": "^6.0.0",
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "angular-srcs",
"version": "21.2.17",
"version": "21.2.22",
"private": true,
"description": "Angular - a web framework for modern web apps",
"homepage": "https://github.com/angular/angular",
@@ -76,9 +76,9 @@
"@angular/service-worker": "workspace:*",
"@angular/ssr": "21.2.9",
"@angular/upgrade": "workspace: *",
"@babel/cli": "7.28.6",
"@babel/core": "7.29.0",
"@babel/generator": "7.29.1",
"@babel/cli": "7.29.7",
"@babel/core": "7.29.7",
"@babel/generator": "7.29.7",
"@jridgewell/sourcemap-codec": "^1.4.14",
"@microsoft/api-extractor": "^7.24.2",
"@rollup/plugin-babel": "^6.0.0",
@@ -116,7 +116,7 @@
"d3": "^7.0.0",
"dagre-d3-es": "^7.0.14",
"diff": "^8.0.0",
"domino": "https://github.com/angular/domino.git#f74cccd496283b6141fbdbeb25b168f4b1c9836a",
"domino": "https://github.com/angular/domino.git#04f987dc08ff3736b427f50941adf1722458528f",
"esbuild": "0.27.2",
"esbuild-plugin-umd-wrapper": "^3.0.0",
"http-server": "^14.0.0",
@@ -163,7 +163,7 @@
"@actions/github": "^9.0.0",
"@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#4de8a14a1682d0f07e0b14a3b26498757c195904",
"@babel/plugin-proposal-async-generator-functions": "7.20.7",
"@babel/plugin-transform-async-generator-functions": "^7.27.1",
"@babel/plugin-transform-async-generator-functions": "^7.29.7",
"@bazel/bazelisk": "^1.7.5",
"@bazel/buildifier": "^8.0.0",
"@bazel/ibazel": "0.28.0",
+8 -4
View File
@@ -100,11 +100,15 @@ export class HttpInterceptorHandler implements HttpHandler {
handle(initialRequest: HttpRequest<any>): Observable<HttpEvent<any>> {
if (this.chain === null) {
const parentHandler = this.injector.get(HttpHandler, null, {skipSelf: true});
const isDelegating = parentHandler !== null && this.backend === parentHandler;
const rootInterceptorFns = this.injector.get(
HTTP_ROOT_INTERCEPTOR_FNS,
[],
isDelegating ? {self: true} : undefined,
);
const dedupedInterceptorFns = Array.from(
new Set([
...this.injector.get(HTTP_INTERCEPTOR_FNS),
...this.injector.get(HTTP_ROOT_INTERCEPTOR_FNS, []),
]),
new Set([...this.injector.get(HTTP_INTERCEPTOR_FNS), ...rootInterceptorFns]),
);
// Note: interceptors are wrapped right-to-left so that final execution order is
+8 -7
View File
@@ -192,10 +192,10 @@ export class HttpHeaders {
private copyFrom(other: HttpHeaders) {
other.init();
Array.from(other.headers.keys()).forEach((key) => {
this.headers.set(key, other.headers.get(key)!);
for (const [key, values] of other.headers.entries()) {
this.headers.set(key, values);
this.normalizedNames.set(key, other.normalizedNames.get(key)!);
});
}
}
private clone(update: Update): HttpHeaders {
@@ -218,21 +218,22 @@ export class HttpHeaders {
return;
}
this.maybeSetNormalizedName(update.name, key);
const base = (update.op === 'a' ? this.headers.get(key) : undefined) || [];
const base = update.op === 'a' ? (this.headers.get(key) || []).slice() : [];
base.push(...value);
this.headers.set(key, base);
break;
case 'd':
const toDelete = update.value as string | undefined;
if (!toDelete) {
const toDelete = update.value;
if (toDelete === undefined) {
this.headers.delete(key);
this.normalizedNames.delete(key);
} else {
const valuesToDelete = Array.isArray(toDelete) ? toDelete : [toDelete];
let existing = this.headers.get(key);
if (!existing) {
return;
}
existing = existing.filter((value) => toDelete.indexOf(value) === -1);
existing = existing.filter((value) => valuesToDelete.indexOf(value) === -1);
if (existing.length === 0) {
this.headers.delete(key);
this.normalizedNames.delete(key);
+2 -1
View File
@@ -20,6 +20,7 @@ import type {HttpHandler} from './backend';
import {HttpRequest} from './request';
import {HttpEvent} from './response';
import {xsrfInterceptorFn} from './xsrf';
/**
* Intercepts and handles an `HttpRequest` or `HttpResponse`.
@@ -200,7 +201,7 @@ export const HTTP_INTERCEPTORS = new InjectionToken<readonly HttpInterceptor[]>(
*/
export const HTTP_INTERCEPTOR_FNS = new InjectionToken<readonly HttpInterceptorFn[]>(
typeof ngDevMode !== 'undefined' && ngDevMode ? 'HTTP_INTERCEPTOR_FNS' : '',
{factory: () => []},
{factory: () => [xsrfInterceptorFn]},
);
/**
+7 -3
View File
@@ -324,18 +324,22 @@ export class HttpParams {
}
if (this.cloneFrom !== null) {
this.cloneFrom.init();
this.cloneFrom.keys().forEach((key) => this.map!.set(key, this.cloneFrom!.map!.get(key)!));
for (const [key, values] of this.cloneFrom.map!.entries()) {
this.map!.set(key, values);
}
this.updates!.forEach((update) => {
switch (update.op) {
case 'a':
case 's':
const base = (update.op === 'a' ? this.map!.get(update.param) : undefined) || [];
const base = update.op === 'a' ? (this.map!.get(update.param) || []).slice() : [];
base.push(valueToString(update.value!));
this.map!.set(update.param, base);
break;
case 'd':
if (update.value !== undefined) {
let base = this.map!.get(update.param) || [];
const base = (this.map!.get(update.param) || []).slice();
const idx = base.indexOf(valueToString(update.value));
if (idx !== -1) {
base.splice(idx, 1);
+12 -3
View File
@@ -101,9 +101,16 @@ export function provideHttpClient(
featureKinds.has(HttpFeatureKind.CustomXsrfConfiguration)
) {
throw new Error(
ngDevMode
? `Configuration error: found both withXsrfConfiguration() and withNoXsrfProtection() in the same call to provideHttpClient(), which is a contradiction.`
: '',
`Configuration error: found both withXsrfConfiguration() and withNoXsrfProtection() in the same call to provideHttpClient(), which is a contradiction.`,
);
}
if (
featureKinds.has(HttpFeatureKind.RequestsMadeViaParent) &&
featureKinds.has(HttpFeatureKind.Fetch)
) {
throw new Error(
`Configuration error: withRequestsMadeViaParent() cannot be combined with withFetch() in the same call to provideHttpClient().`,
);
}
}
@@ -260,6 +267,8 @@ export function withJsonpSupport(): HttpFeature<HttpFeatureKind.JsonpSupport> {
* "bubble up" until either reaching the root level or an `HttpClient` which was not configured with
* this option.
*
* This feature cannot be combined with `withFetch` in the same `provideHttpClient()` call.
*
* @see [HTTP client setup](guide/http/setup#withrequestsmadeviaparent)
* @see {@link provideHttpClient}
* @publicApi 19.0
+13 -7
View File
@@ -288,8 +288,10 @@ export function transferCacheInterceptorFn(
const {headers, body, status, statusText} = event;
// Only cache successful HTTP responses that do not have Cache-Control
// directives that forbid shared caching (no-store or private).
if (hasUncacheableCacheControl(headers)) {
// directives that forbid shared caching (no-store or private) and do not
// carry a Set-Cookie header. A Set-Cookie header marks the response as
// user-specific.
if (hasUncacheableCacheControl(headers) || hasSetCookieHeader(headers)) {
return;
}
@@ -344,6 +346,10 @@ function hasUncacheableCacheControl(headers: HttpHeaders): boolean {
});
}
function hasSetCookieHeader(headers: HttpHeaders): boolean {
return headers.has('set-cookie');
}
function isNonCacheableRequest(cache: RequestCache): boolean {
return cache === 'no-cache' || cache === 'no-store';
}
@@ -368,17 +374,17 @@ function getFilteredHeaders(
}
function sortAndConcatParams(params: HttpParams | URLSearchParams): string {
return [...params.keys()]
.sort()
.map((k) => `${k}=${params.getAll(k)}`)
.join('&');
const searchParams = new URLSearchParams(
params instanceof URLSearchParams ? params : params.toString(),
);
searchParams.sort();
return searchParams.toString();
}
function makeCacheKey(
request: HttpRequest<any>,
mappedRequestUrl: string,
): StateKey<TransferHttpResponse> {
// make the params encoded same as a url so it's easy to identify
const {params, method, responseType} = request;
const encodedParams = sortAndConcatParams(params);
+2 -2
View File
@@ -16,8 +16,8 @@ import {
} from '@angular/core';
import {Observable} from 'rxjs';
import {HttpHandler} from './backend';
import {HttpHandlerFn, HttpInterceptor} from './interceptor';
import type {HttpHandler} from './backend';
import type {HttpHandlerFn, HttpInterceptor} from './interceptor';
import {HttpRequest} from './request';
import {HttpEvent} from './response';
+37
View File
@@ -154,6 +154,34 @@ describe('HttpHeaders', () => {
const fourth = third.delete('FOO');
expect(fourth.has('foo')).toEqual(false);
});
it('should delete only the exact matching string value', () => {
const headers = new HttpHeaders({
'X-Scopes': ['tenant:alpha', 'tenant:alpha:archive'],
});
const updated = headers.delete('X-Scopes', 'tenant:alpha:archive');
expect(updated.getAll('X-Scopes')).toEqual(['tenant:alpha']);
});
it('should delete only exact matching values from an array', () => {
const headers = new HttpHeaders({
'X-Scopes': ['tenant:alpha', 'tenant:alpha:archive'],
});
const updated = headers.delete('X-Scopes', ['tenant:alpha:archive']);
expect(updated.getAll('X-Scopes')).toEqual(['tenant:alpha']);
});
it('should treat an empty string as a value to delete', () => {
const headers = new HttpHeaders({foo: ['', 'bar']});
const updated = headers.delete('foo', '');
expect(updated.getAll('foo')).toEqual(['bar']);
});
});
describe('.append', () => {
@@ -165,6 +193,15 @@ describe('HttpHeaders', () => {
expect(third.getAll('foo')).toEqual(['bar', 'baz']);
});
it('should not mutate a materialized source', () => {
const source = new HttpHeaders().set('foo', 'bar');
expect(source.getAll('foo')).toEqual(['bar']);
const clone = source.append('foo', 'baz');
expect(clone.getAll('foo')).toEqual(['bar', 'baz']);
expect(source.getAll('foo')).toEqual(['bar']);
});
it('should preserve the case of the first call', () => {
const headers = new HttpHeaders();
const second = headers.append('FOO', 'bar');
+18
View File
@@ -72,6 +72,15 @@ describe('HttpUrlEncodedParams', () => {
expect(mutated.toString()).toEqual('a=b&a=true');
});
it('should not mutate a materialized source when appending', () => {
const source = new HttpParams().append('a', 'b');
expect(source.getAll('a')).toEqual(['b']);
const clone = source.append('a', 'c');
expect(clone.getAll('a')).toEqual(['b', 'c']);
expect(source.getAll('a')).toEqual(['b']);
});
it('should allow appending all string parameters', () => {
const body = new HttpParams({fromString: 'a=a1&b=b1'});
const mutated = body.appendAll({a: ['a2', 'a3'], b: 'b2'});
@@ -138,6 +147,15 @@ describe('HttpUrlEncodedParams', () => {
expect(mutated.getAll('a')).toEqual(['1', '3', '5']);
});
it('should not mutate a materialized source when deleting a value', () => {
const source = new HttpParams().append('a', '1').append('a', '2');
expect(source.getAll('a')).toEqual(['1', '2']);
const clone = source.delete('a', '1');
expect(clone.getAll('a')).toEqual(['2']);
expect(source.getAll('a')).toEqual(['1', '2']);
});
it('should allow deletion of one value of a number parameter', () => {
const body = new HttpParams({fromString: 'a=0&a=1&a=2&a=3&a=4&a=5'});
const mutated = body.delete('a', 0).delete('a', 4);
+183 -8
View File
@@ -34,8 +34,9 @@ import {
} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {EMPTY, Observable, from} from 'rxjs';
import {tap} from 'rxjs/operators';
import {HttpInterceptorFn} from '../src/interceptor';
import {HTTP_ROOT_INTERCEPTOR_FNS, HttpInterceptorFn} from '../src/interceptor';
import {
HttpFeature,
HttpFeatureKind,
@@ -264,6 +265,19 @@ describe('provideHttpClient', () => {
});
describe('xsrf protection', () => {
it('should enable xsrf protection for the root-provided HttpClient', () => {
TestBed.configureTestingModule({
providers: [provideHttpClientTesting(), {provide: PLATFORM_ID, useValue: 'test'}],
});
setXsrfToken('abcdefg');
TestBed.inject(HttpClient).post('/test', '', {responseType: 'text'}).subscribe();
const req = TestBed.inject(HttpTestingController).expectOne('/test');
expect(req.request.headers.get('X-XSRF-TOKEN')).toEqual('abcdefg');
req.flush('');
});
it('should enable xsrf protection by default', () => {
TestBed.configureTestingModule({
providers: [
@@ -314,7 +328,7 @@ describe('provideHttpClient', () => {
TestBed.inject(HttpClient).post('/test', '', {responseType: 'text'}).subscribe();
const req = TestBed.inject(HttpTestingController).expectOne('/test');
expect(req.request.headers.has('X-Custom-Xsrf-Header')).toBeFalse();
expect(req.request.headers.has('X-XSRF-TOKEN')).toBeFalse();
req.flush('');
});
@@ -359,6 +373,12 @@ describe('provideHttpClient', () => {
});
describe('withRequestsMadeViaParent()', () => {
it('should error when combined with a backend override', () => {
expect(() => provideHttpClient(withRequestsMadeViaParent(), withFetch())).toThrowError(
'Configuration error: withRequestsMadeViaParent() cannot be combined with withFetch() in the same call to provideHttpClient().',
);
});
for (const backend of ['fetch', 'xhr']) {
describe(`given '${backend}' backend`, () => {
const commonHttpFeatures: HttpFeature<HttpFeatureKind>[] = [];
@@ -436,18 +456,59 @@ describe('provideHttpClient', () => {
req.flush('');
});
it('should be able to connect to a legacy-provided HttpClient context', () => {
it('should include root interceptors in independent child contexts', () => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [provideLegacyInterceptor('parent')],
providers: [
provideHttpClient(...commonHttpFeatures),
{
provide: HTTP_ROOT_INTERCEPTOR_FNS,
useValue: makeLiteralTagInterceptorFn('root'),
multi: true,
},
provideHttpClientTesting(),
],
});
const child = createEnvironmentInjector(
[
provideHttpClient(withInterceptors([makeLiteralTagInterceptorFn('child')])),
{
provide: HttpBackend,
useFactory: () => inject(HttpBackend, {skipSelf: true}),
},
],
TestBed.inject(EnvironmentInjector),
);
child.get(HttpClient).get('/test', {responseType: 'text'}).subscribe();
const req = TestBed.inject(HttpTestingController).expectOne('/test');
expect(req.request.headers.get('X-Tag')).toEqual('child,root');
req.flush('');
});
it('should run root interceptors once in the parent request and response chain', () => {
const order: string[] = [];
TestBed.configureTestingModule({
providers: [
provideHttpClient(
...commonHttpFeatures,
withInterceptors([makeOrderedTagInterceptorFn('parent', order)]),
),
{
provide: HTTP_ROOT_INTERCEPTOR_FNS,
useValue: makeOrderedTagInterceptorFn('root', order),
multi: true,
},
provideHttpClientTesting(),
],
});
const child = createEnvironmentInjector(
[
provideHttpClient(
...commonHttpFeatures,
withRequestsMadeViaParent(),
withInterceptors([makeLiteralTagInterceptorFn('child')]),
withInterceptors([makeOrderedTagInterceptorFn('child', order)]),
),
],
TestBed.inject(EnvironmentInjector),
@@ -455,11 +516,112 @@ describe('provideHttpClient', () => {
child.get(HttpClient).get('/test', {responseType: 'text'}).subscribe();
const req = TestBed.inject(HttpTestingController).expectOne('/test');
expect(req.request.headers.get('X-Tag')).toEqual('child,parent');
expect(req.request.headers.get('X-Tag')).toEqual('child,parent,root');
expect(order).toEqual(['child:request', 'parent:request', 'root:request']);
req.flush('');
expect(order).toEqual([
'child:request',
'parent:request',
'root:request',
'root:response',
'parent:response',
'child:response',
]);
});
});
}
it('should be able to connect to a legacy-provided HttpClient context', () => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [provideLegacyInterceptor('parent')],
});
const child = createEnvironmentInjector(
[
provideHttpClient(
withRequestsMadeViaParent(),
withInterceptors([makeLiteralTagInterceptorFn('child')]),
),
],
TestBed.inject(EnvironmentInjector),
);
child.get(HttpClient).get('/test', {responseType: 'text'}).subscribe();
const req = TestBed.inject(HttpTestingController).expectOne('/test');
expect(req.request.headers.get('X-Tag')).toEqual('child,parent');
req.flush('');
});
it('should inherit root interceptors in independent child contexts', () => {
TestBed.configureTestingModule({
providers: [
provideHttpClient(),
{
provide: HTTP_ROOT_INTERCEPTOR_FNS,
useValue: makeLiteralTagInterceptorFn('root'),
multi: true,
},
provideHttpClientTesting(),
],
});
const delegatingParent = createEnvironmentInjector(
[provideHttpClient(withRequestsMadeViaParent())],
TestBed.inject(EnvironmentInjector),
);
const independentChild = createEnvironmentInjector(
[
provideHttpClient(withInterceptors([makeLiteralTagInterceptorFn('child')])),
{provide: HttpBackend, useValue: TestBed.inject(HttpBackend)},
],
delegatingParent,
);
try {
independentChild.get(HttpClient).get('/test', {responseType: 'text'}).subscribe();
const req = TestBed.inject(HttpTestingController).expectOne('/test');
expect(req.request.headers.get('X-Tag')).toEqual('child,root');
req.flush('');
} finally {
independentChild.destroy();
delegatingParent.destroy();
}
});
it('should inherit root interceptors when a custom provider overrides delegation', () => {
TestBed.configureTestingModule({
providers: [
provideHttpClient(),
{
provide: HTTP_ROOT_INTERCEPTOR_FNS,
useValue: makeLiteralTagInterceptorFn('root'),
multi: true,
},
provideHttpClientTesting(),
],
});
const child = createEnvironmentInjector(
[
provideHttpClient(
withRequestsMadeViaParent(),
withInterceptors([makeLiteralTagInterceptorFn('child')]),
),
{provide: HttpBackend, useValue: TestBed.inject(HttpBackend)},
],
TestBed.inject(EnvironmentInjector),
);
try {
child.get(HttpClient).get('/test', {responseType: 'text'}).subscribe();
const req = TestBed.inject(HttpTestingController).expectOne('/test');
expect(req.request.headers.get('X-Tag')).toEqual('child,root');
req.flush('');
} finally {
child.destroy();
}
});
});
describe('compatibility with Http NgModules', () => {
@@ -641,6 +803,19 @@ function makeLiteralTagInterceptorFn(tag: string): HttpInterceptorFn {
return (req, next) => next(addTagToRequest(req, tag));
}
function makeOrderedTagInterceptorFn(tag: string, order: string[]): HttpInterceptorFn {
return (req, next) => {
order.push(`${tag}:request`);
return next(addTagToRequest(req, tag)).pipe(
tap((event) => {
if (event instanceof HttpResponse) {
order.push(`${tag}:response`);
}
}),
);
};
}
function makeTokenTagInterceptorFn(tag: InjectionToken<string>): HttpInterceptorFn {
return (req, next) => next(addTagToRequest(req, inject(tag)));
}
@@ -10,6 +10,8 @@ import {DOCUMENT} from '../../index';
import {
ApplicationRef,
Component,
createEnvironmentInjector,
EnvironmentInjector,
Injectable,
PLATFORM_ID,
TransferState,
@@ -19,7 +21,16 @@ import {TestBed} from '@angular/core/testing';
import {useAutoTick, timeout, withBody} from '@angular/private/testing';
import {BehaviorSubject, Observable, of} from 'rxjs';
import {HttpClient, HttpHeaders, HttpRequest, HttpResponse, provideHttpClient} from '../public_api';
import {
HttpClient,
HttpHeaders,
HttpParams,
HttpRequest,
HttpResponse,
provideHttpClient,
withInterceptors,
withRequestsMadeViaParent,
} from '../public_api';
import {
BODY,
CACHE_OPTIONS,
@@ -215,6 +226,32 @@ describe('TransferCache', () => {
expect(secondNext).toHaveBeenCalledTimes(1);
});
it('should not cache responses with a Set-Cookie header', () => {
configureInterceptor();
const request = new HttpRequest('GET', '/test-set-cookie');
const firstNext = jasmine.createSpy('firstNext').and.returnValue(
of(
new HttpResponse({
body: 'user-a-session',
headers: new HttpHeaders({'Set-Cookie': 'session=user-a; HttpOnly'}),
}),
),
);
const secondNext = jasmine
.createSpy('secondNext')
.and.returnValue(of(new HttpResponse({body: 'user-b-session'})));
runOnServer(() => {
expect(runInterceptor(request, firstNext).body).toBe('user-a-session');
expect(runInterceptor(request, secondNext).body).toBe('user-b-session');
});
expect(firstNext).toHaveBeenCalledTimes(1);
expect(secondNext).toHaveBeenCalledTimes(1);
});
it('should not cache requests with Cache-Control: no-store', () => {
configureInterceptor();
@@ -262,6 +299,124 @@ describe('TransferCache', () => {
});
});
describe('withRequestsMadeViaParent()', () => {
let childInjector: EnvironmentInjector;
beforeEach(() => {
globalThis['ngServerMode'] = true;
TestBed.configureTestingModule({
providers: [
TransferState,
withHttpTransferCache({
filter: (request) => !request.headers.has('X-API-Key'),
}),
provideHttpClient(
withInterceptors([
(request, next) => {
if (request.url === '/private') {
request = request.clone({
setHeaders: {Authorization: 'Bearer server-token'},
});
} else if (request.url === '/secret') {
request = request.clone({setHeaders: {'X-API-Key': 'secret-a'}});
} else if (request.url === '/public-alias') {
request = request.clone({url: '/public'});
}
return next(request);
},
]),
),
provideHttpClientTesting(),
],
});
childInjector = createEnvironmentInjector(
[
provideHttpClient(
withInterceptors([
(request, next) =>
next(request.clone({setHeaders: {'X-Trace-Id': 'feature-request'}})),
]),
withRequestsMadeViaParent(),
),
],
TestBed.inject(EnvironmentInjector),
);
});
afterEach(() => {
childInjector.destroy();
TestBed.inject(HttpTestingController).verify();
globalThis['ngServerMode'] = undefined;
});
it('should evaluate cache eligibility and keys after parent interceptors', () => {
const httpClient = childInjector.get(HttpClient);
const httpTestingController = TestBed.inject(HttpTestingController);
const transferState = TestBed.inject(TransferState);
let privateResponse: unknown;
httpClient.get('/private').subscribe((response) => (privateResponse = response));
const privateRequest = httpTestingController.expectOne('/private');
expect(privateRequest.request.headers.get('Authorization')).toBe('Bearer server-token');
expect(privateRequest.request.headers.get('X-Trace-Id')).toBe('feature-request');
privateRequest.flush({internalSecret: 'server-only'});
expect(privateResponse).toEqual({internalSecret: 'server-only'});
expect(JSON.parse(transferState.toJson())).toEqual({});
let publicResponse: unknown;
httpClient.get('/public-alias').subscribe((response) => (publicResponse = response));
httpTestingController.expectOne('/public').flush({message: 'public'});
publicResponse = undefined;
httpClient.get('/public-alias').subscribe((response) => (publicResponse = response));
httpTestingController.expectNone('/public');
expect(publicResponse).toEqual({message: 'public'});
});
it('should evaluate cache filters after parent interceptors', () => {
const httpClient = childInjector.get(HttpClient);
const httpTestingController = TestBed.inject(HttpTestingController);
const transferState = TestBed.inject(TransferState);
let response: unknown;
httpClient.get('/secret').subscribe((value) => (response = value));
const request = httpTestingController.expectOne('/secret');
expect(request.request.headers.get('X-API-KEY')).toBe('secret-a');
request.flush({internalSecret: 'secret-only'});
expect(response).toEqual({internalSecret: 'secret-only'});
expect(JSON.parse(transferState.toJson())).toEqual({});
});
it('should evaluate cache eligibility through multiple parent clients', () => {
const grandchildInjector = createEnvironmentInjector(
[provideHttpClient(withRequestsMadeViaParent())],
childInjector,
);
try {
const httpClient = grandchildInjector.get(HttpClient);
const httpTestingController = TestBed.inject(HttpTestingController);
const transferState = TestBed.inject(TransferState);
let privateResponse: unknown;
httpClient.get('/private').subscribe((response) => (privateResponse = response));
const privateRequest = httpTestingController.expectOne('/private');
expect(privateRequest.request.headers.get('Authorization')).toBe('Bearer server-token');
privateRequest.flush({internalSecret: 'server-only'});
expect(privateResponse).toEqual({internalSecret: 'server-only'});
expect(JSON.parse(transferState.toJson())).toEqual({});
} finally {
grandchildInjector.destroy();
}
});
});
describe('withHttpTransferCache', () => {
let isStable: BehaviorSubject<boolean>;
@@ -432,6 +587,38 @@ describe('TransferCache', () => {
);
});
it('should differentiate repeated parameters from scalar comma parameters', () => {
let scalarResponse!: string;
TestBed.inject(HttpClient)
.get('/test-params', {params: new HttpParams().set('role', 'user,admin')})
.subscribe((response) => (scalarResponse = response as string));
TestBed.inject(HttpTestingController)
.expectOne('/test-params?role=user,admin')
.flush('scalar');
let repeatedResponse!: string;
TestBed.inject(HttpClient)
.get('/test-params', {
params: new HttpParams().append('role', 'user').append('role', 'admin'),
})
.subscribe((response) => (repeatedResponse = response as string));
TestBed.inject(HttpTestingController)
.expectOne('/test-params?role=user&role=admin')
.flush('repeated');
let repeatedCachedResponse!: string;
TestBed.inject(HttpClient)
.get('/test-params', {
params: new HttpParams().append('role', 'user').append('role', 'admin'),
})
.subscribe((response) => (repeatedCachedResponse = response as string));
TestBed.inject(HttpTestingController).expectNone('/test-params?role=user&role=admin');
expect(scalarResponse).toBe('scalar');
expect(repeatedResponse).toBe('repeated');
expect(repeatedCachedResponse).toBe('repeated');
});
it('should skip cache when specified', () => {
makeRequestAndExpectOne('/test-1?foo=1', 'foo', {transferCache: false});
// The previous request wasn't cached so this one can't use the cache
@@ -591,6 +778,14 @@ describe('TransferCache', () => {
makeRequestAndExpectOne('/test-private', 'fresh-data');
});
it('should not cache responses with a Set-Cookie header', () => {
makeRequestAndExpectOne('/test-set-cookie', 'user-a-session', {
responseHeaders: {'Set-Cookie': 'session=user-a; HttpOnly'},
});
makeRequestAndExpectOne('/test-set-cookie', 'user-b-session');
});
it('should not cache responses with Cache-Control containing no-store among other directives', () => {
makeRequestAndExpectOne('/test-multi', 'data', {
responseHeaders: {'Cache-Control': 'max-age=0, no-store, must-revalidate'},
+1 -1
View File
@@ -30,7 +30,7 @@
}
},
"dependencies": {
"@babel/core": "7.29.0",
"@babel/core": "7.29.7",
"@jridgewell/sourcemap-codec": "^1.4.14",
"reflect-metadata": "^0.2.0",
"chokidar": "^5.0.0",
@@ -936,6 +936,48 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE
},
}]
}] });
export class HostBindingCustomSrcdocDir {
evil = 'evil';
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingCustomSrcdocDir, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: HostBindingCustomSrcdocDir, isStandalone: true, selector: "safe-srcdoc-carrier", host: { properties: { "attr.srcdoc": "evil" } }, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingCustomSrcdocDir, decorators: [{
type: Directive,
args: [{
selector: 'safe-srcdoc-carrier',
host: {
'[attr.srcdoc]': 'evil',
},
}]
}] });
export class HostBindingCustomSrcDir {
evil = 'evil';
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingCustomSrcDir, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: HostBindingCustomSrcDir, isStandalone: true, selector: "safe-src-carrier", host: { properties: { "attr.src": "evil" } }, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingCustomSrcDir, decorators: [{
type: Directive,
args: [{
selector: 'safe-src-carrier',
host: {
'[attr.src]': 'evil',
},
}]
}] });
export class HostBindingCustomDataDir {
evil = 'evil';
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingCustomDataDir, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: HostBindingCustomDataDir, isStandalone: true, selector: "safe-data-carrier", host: { properties: { "attr.data": "evil" } }, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingCustomDataDir, decorators: [{
type: Directive,
args: [{
selector: 'safe-data-carrier',
host: {
'[attr.data]': 'evil',
},
}]
}] });
/****************************************************************************************************
* PARTIAL FILE: sanitization.d.ts
@@ -963,6 +1005,21 @@ export declare class HostBindingSvgAnimateDir {
static ɵfac: i0.ɵɵFactoryDeclaration<HostBindingSvgAnimateDir, never>;
static ɵdir: i0.ɵɵDirectiveDeclaration<HostBindingSvgAnimateDir, "animateMotion[hostBindingSvgAnimateDir]", never, {}, {}, never, never, true, never>;
}
export declare class HostBindingCustomSrcdocDir {
evil: string;
static ɵfac: i0.ɵɵFactoryDeclaration<HostBindingCustomSrcdocDir, never>;
static ɵdir: i0.ɵɵDirectiveDeclaration<HostBindingCustomSrcdocDir, "safe-srcdoc-carrier", never, {}, {}, never, never, true, never>;
}
export declare class HostBindingCustomSrcDir {
evil: string;
static ɵfac: i0.ɵɵFactoryDeclaration<HostBindingCustomSrcDir, never>;
static ɵdir: i0.ɵɵDirectiveDeclaration<HostBindingCustomSrcDir, "safe-src-carrier", never, {}, {}, never, never, true, never>;
}
export declare class HostBindingCustomDataDir {
evil: string;
static ɵfac: i0.ɵɵFactoryDeclaration<HostBindingCustomDataDir, never>;
static ɵdir: i0.ɵɵDirectiveDeclaration<HostBindingCustomDataDir, "safe-data-carrier", never, {}, {}, never, never, true, never>;
}
/****************************************************************************************************
* PARTIAL FILE: security_sensitive_constant_attributes.js
@@ -1,21 +1,21 @@
hostBindings: function HostBindingLinkDir_HostBindings(rf, ctx) {
if (rf & 2) {
$r3$.ɵɵdomProperty("innerHTML", ctx.evil, $r3$.ɵɵsanitizeHtml)("href", ctx.evil, $r3$.ɵɵsanitizeUrl);
$r3$.ɵɵdomProperty("innerHTML", ctx.evil, $r3$.ɵɵsanitizeHtml)("href", ctx.evil, $r3$.ɵɵsanitizeUrlOrResourceUrl);
$r3$.ɵɵattribute("style", ctx.evil, $r3$.ɵɵsanitizeStyle);
}
}
…
hostBindings: function HostBindingImageDir_HostBindings(rf, ctx) {
if (rf & 2) {
i0.ɵɵdomProperty("innerHTML", ctx.evil, i0.ɵɵsanitizeHtml)("src", ctx.nonEvil, i0.ɵɵsanitizeUrl);
i0.ɵɵdomProperty("innerHTML", ctx.evil, i0.ɵɵsanitizeHtml)("src", ctx.nonEvil, i0.ɵɵsanitizeUrlOrResourceUrl);
i0.ɵɵattribute("style", ctx.evil, i0.ɵɵsanitizeStyle);
}
}
…
hostBindings: function HostBindingIframeDir_HostBindings(rf, ctx) {
if (rf & 2) {
$r3$.ɵɵdomProperty("innerHTML", ctx.evil, $r3$.ɵɵsanitizeHtml)("src", ctx.evil, i0.ɵɵsanitizeResourceUrl)("sandbox", ctx.evil, $r3$.ɵɵvalidateAttribute);
$r3$.ɵɵattribute("style", ctx.evil, $r3$.ɵɵsanitizeStyle)("attributeName", ctx.nonEvil);
$r3$.ɵɵdomProperty("innerHTML", ctx.evil, $r3$.ɵɵsanitizeHtml)("src", ctx.evil, i0.ɵɵsanitizeUrlOrResourceUrl)("sandbox", ctx.evil, $r3$.ɵɵvalidateAttribute);
$r3$.ɵɵattribute("style", ctx.evil, $r3$.ɵɵsanitizeStyle)("attributeName", ctx.nonEvil, i0.ɵɵvalidateAttribute);
}
}
…
@@ -24,3 +24,21 @@ hostBindings: function HostBindingSvgAnimateDir_HostBindings(rf, ctx) {
i0.ɵɵattribute("attributeName", ctx.evil, i0.ɵɵvalidateAttribute);
}
}
…
hostBindings: function HostBindingCustomSrcdocDir_HostBindings(rf, ctx) {
if (rf & 2) {
i0.ɵɵattribute("srcdoc", ctx.evil, i0.ɵɵsanitizeHtml);
}
}
…
hostBindings: function HostBindingCustomSrcDir_HostBindings(rf, ctx) {
if (rf & 2) {
i0.ɵɵattribute("src", ctx.evil, i0.ɵɵsanitizeUrlOrResourceUrl);
}
}
…
hostBindings: function HostBindingCustomDataDir_HostBindings(rf, ctx) {
if (rf & 2) {
i0.ɵɵattribute("data", ctx.evil, i0.ɵɵsanitizeUrlOrResourceUrl);
}
}
@@ -49,3 +49,33 @@ export class HostBindingIframeDir {
export class HostBindingSvgAnimateDir {
evil = 'evil';
}
@Directive({
selector: 'safe-srcdoc-carrier',
host: {
'[attr.srcdoc]': 'evil',
},
})
export class HostBindingCustomSrcdocDir {
evil = 'evil';
}
@Directive({
selector: 'safe-src-carrier',
host: {
'[attr.src]': 'evil',
},
})
export class HostBindingCustomSrcDir {
evil = 'evil';
}
@Directive({
selector: 'safe-data-carrier',
host: {
'[attr.data]': 'evil',
},
})
export class HostBindingCustomDataDir {
evil = 'evil';
}
+37 -5
View File
@@ -8569,7 +8569,7 @@ runInEachFileSystem((os: string) => {
hostVars: 6,
hostBindings: function UnsafeAttrsDirective_HostBindings(rf, ctx) {
if (rf & 2) {
i0.ɵɵattribute("href", ctx.attrHref, i0.ɵɵsanitizeUrlOrResourceUrl)("src", ctx.attrSrc, i0.ɵɵsanitizeUrlOrResourceUrl)("action", ctx.attrAction, i0.ɵɵsanitizeUrl)("profile", ctx.attrProfile)("innerHTML", ctx.attrInnerHTML, i0.ɵɵsanitizeHtml)("title", ctx.attrSafeTitle);
i0.ɵɵattribute("href", ctx.attrHref, i0.ɵɵsanitizeUrlOrResourceUrl)("src", ctx.attrSrc, i0.ɵɵsanitizeUrlOrResourceUrl)("action", ctx.attrAction, i0.ɵɵsanitizeUrlOrResourceUrl)("profile", ctx.attrProfile)("innerHTML", ctx.attrInnerHTML, i0.ɵɵsanitizeHtml)("title", ctx.attrSafeTitle);
}
}
`;
@@ -8615,14 +8615,14 @@ runInEachFileSystem((os: string) => {
hostVars: 3,
hostBindings: function UnsafePropsDirective_HostBindings(rf, ctx) {
if (rf & 2) {
i0.ɵɵdomProperty("href", ctx.propHref, i0.ɵɵsanitizeUrl)("innerHTML", ctx.propInnerHTML, i0.ɵɵsanitizeHtml)("title", ctx.propSafeTitle);
i0.ɵɵdomProperty("href", ctx.propHref, i0.ɵɵsanitizeUrlOrResourceUrl)("innerHTML", ctx.propInnerHTML, i0.ɵɵsanitizeHtml)("title", ctx.propSafeTitle);
}
}
`;
expect(trim(jsContents)).toContain(trim(hostBindingsFn));
});
it('should not generate sanitizers for URL properties in hostBindings fn in Component', () => {
it('should generate concrete-host URL sanitizers in hostBindings fn in Component', () => {
env.write(
`test.ts`,
`
@@ -8655,8 +8655,40 @@ runInEachFileSystem((os: string) => {
hostVars: 5,
hostBindings: function FooCmp_HostBindings(rf, ctx) {
if (rf & 2) {
i0.ɵɵdomProperty("href", ctx.hrefProp, i0.ɵɵsanitizeUrl)("title", ctx.titleProp);
i0.ɵɵattribute("src", ctx.srcAttr)("href", ctx.hrefAttr, i0.ɵɵsanitizeUrl)("title", ctx.titleAttr);
i0.ɵɵdomProperty("href", ctx.hrefProp, i0.ɵɵsanitizeUrlOrResourceUrl)("title", ctx.titleProp);
i0.ɵɵattribute("src", ctx.srcAttr, i0.ɵɵsanitizeUrlOrResourceUrl)("href", ctx.hrefAttr, i0.ɵɵsanitizeUrlOrResourceUrl)("title", ctx.titleAttr);
}
}
`;
expect(trim(jsContents)).toContain(trim(hostBindingsFn));
});
it('should generate sanitizers for pure :not selector host bindings', () => {
env.write(
`test.ts`,
`
import {Component} from '@angular/core';
@Component({
selector: ':not(iframe)',
template: '',
host: {
'[attr.srcdoc]': 'srcdoc',
}
})
class FooCmp {
srcdoc: any;
}
`,
);
env.driveMain();
const jsContents = env.getContents('test.js');
const hostBindingsFn = `
hostVars: 1,
hostBindings: function FooCmp_HostBindings(rf, ctx) {
if (rf & 2) {
i0.ɵɵattribute("srcdoc", ctx.srcdoc, i0.ɵɵsanitizeHtml);
}
}
`;
@@ -208,7 +208,7 @@ export class I18nMetaVisitor implements html.Visitor {
isTrustedType = isTrustedTypesSink(node.name, name);
}
if (isTrustedType) {
if (isTrustedType || isPossibleEventHandler(name)) {
this._reportError(
attr,
`Translating attribute '${name}' is disallowed for security reasons.`,
@@ -350,3 +350,14 @@ export function i18nMetaToJSDoc(meta: I18nMeta): o.JSDocComment {
}
return o.jsDocComment(tags);
}
/**
* Check if the propertyName is a potential event handler.
* We consider a property to be a potential event handler if its name is longer than 2 characters and starts with 'on' (e.g. 'onclick', 'onload', etc.).
* @param propertyName The name of the property to check.
* @returns True if the property is a potential event handler, false otherwise.
*/
function isPossibleEventHandler(propertyName: string): boolean {
const name = propertyName.toLowerCase();
return name.length > 2 && name !== 'only' && name.startsWith('on');
}
@@ -455,10 +455,12 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry {
const normalizedTag = normalizeTagName(tagName);
propName = propName.toLowerCase();
const [namespace] = splitNsName(normalizedTag, false);
const securitySchema = SECURITY_SCHEMA();
const ctx =
securitySchema[normalizedTag + '|' + propName] ??
(namespace ? securitySchema[`:${namespace}:*|${propName}`] : undefined) ??
securitySchema['*|' + propName] ??
SecurityContext.NONE;
@@ -39,8 +39,8 @@ export enum SecurityContext {
* Map from tagName|propertyName to SecurityContext. Properties applying to all tags use '*'.
*/
let _SECURITY_SCHEMA!: {[k: string]: SecurityContext};
const SVG_NAMESPACE = 'svg';
const MATH_ML_NAMESPACE = 'math';
export const SVG_NAMESPACE = 'svg';
export const MATH_ML_NAMESPACE = 'math';
/**
* @remarks Keep is a copy of DOM Security Schema.
@@ -71,6 +71,7 @@ export function SECURITY_SCHEMA(): {[k: string]: SecurityContext} {
registerContext(SecurityContext.URL, MATH_ML_NAMESPACE, [
// MathML namespace
// https://crsrc.org/c/third_party/blink/renderer/core/sanitizer/sanitizer.cc;l=753-768;drc=b3eb16372dcd3317d65e9e0265015e322494edcd;bpv=1;bpt=1
['*', ['href', 'xlink:href']],
['annotation', ['href', 'xlink:href']],
['annotation-xml', ['href', 'xlink:href']],
['maction', ['href', 'xlink:href']],
@@ -160,8 +161,7 @@ function registerContext(
specs: readonly [tagName: string, attributeNames: readonly string[]][],
): void {
for (const [element, attributeNames] of specs) {
let tagName =
namespace && element !== '*' && element !== 'unknown' ? `:${namespace}:${element}` : element;
let tagName = namespace && element !== 'unknown' ? `:${namespace}:${element}` : element;
tagName = tagName.toLowerCase();
for (const attr of attributeNames) {
@@ -169,3 +169,28 @@ function registerContext(
}
}
}
export function checkSecurityContext(
tagName: string,
propName: string,
namespace?: string | null,
): SecurityContext {
const schema = SECURITY_SCHEMA();
const normalizedTagName = tagName.toLowerCase();
const normalizedPropName = propName.toLowerCase();
const namespacedContext =
namespace && normalizedTagName !== '*' && normalizedTagName !== 'unknown'
? schema[`:${namespace}:${normalizedTagName}|${normalizedPropName}`]
: undefined;
const namespacedWildcardContext = namespace
? schema[`:${namespace}:*|${normalizedPropName}`]
: undefined;
return (
namespacedContext ??
namespacedWildcardContext ??
schema[`${normalizedTagName}|${normalizedPropName}`] ??
schema[`*|${normalizedPropName}`] ??
SecurityContext.NONE
);
}
@@ -17,7 +17,7 @@ import * as t from '../../../render3/r3_ast';
import {DeferBlockDepsEmitMode, R3ComponentDeferMetadata} from '../../../render3/view/api';
import {icuFromI18nMessage} from '../../../render3/view/i18n/util';
import {DomElementSchemaRegistry} from '../../../schema/dom_element_schema_registry';
import {BindingParser} from '../../../template_parser/binding_parser';
import {BindingParser, calcPossibleSecurityContexts} from '../../../template_parser/binding_parser';
import * as ir from '../ir';
import {
@@ -115,19 +115,21 @@ export function ingestHostBinding(
if (property.isAnimation) {
bindingKind = ir.BindingKind.Animation;
}
const securityContexts = bindingParser
.calcPossibleSecurityContexts(
input.componentSelector,
property.name,
bindingKind === ir.BindingKind.Attribute,
)
.filter((context) => context !== SecurityContext.NONE);
const securityContexts = calcHostBindingSecurityContexts(
bindingParser,
input.componentSelector,
property.name,
bindingKind === ir.BindingKind.Attribute,
);
ingestDomProperty(job, property, bindingKind, securityContexts);
}
for (const [name, expr] of Object.entries(input.attributes) ?? []) {
const securityContexts = bindingParser
.calcPossibleSecurityContexts(input.componentSelector, name, true)
.filter((context) => context !== SecurityContext.NONE);
const securityContexts = calcHostBindingSecurityContexts(
bindingParser,
input.componentSelector,
name,
true,
);
ingestHostAttribute(job, name, expr, securityContexts);
}
for (const event of input.events ?? []) {
@@ -136,6 +138,42 @@ export function ingestHostBinding(
return job;
}
function calcHostBindingSecurityContexts(
bindingParser: BindingParser,
selector: string,
name: string,
isAttribute: boolean,
): SecurityContext[] {
const declaringSelectorContexts = bindingParser.calcPossibleSecurityContexts(
selector,
name,
isAttribute,
);
const concreteHostContexts = calcPossibleSecurityContexts(
domSchema,
null,
domSchema.getMappedPropName(name),
isAttribute,
);
const concreteHostNonNoneContexts = concreteHostContexts.filter(
(context) => context !== SecurityContext.NONE,
);
const concreteHostNonNoneCount = concreteHostNonNoneContexts.length;
const hasConcreteHostNoneContext = concreteHostNonNoneCount !== concreteHostContexts.length;
// Host bindings can run against a concrete host whose element name differs from the declaring
// selector, including dynamic root components whose TNode name is `#host`.
if (hasConcreteHostNoneContext && concreteHostNonNoneCount > 0) {
return concreteHostContexts;
}
if (concreteHostNonNoneContexts.some((context) => !declaringSelectorContexts.includes(context))) {
return concreteHostContexts;
}
return declaringSelectorContexts.filter((context) => context !== SecurityContext.NONE);
}
// TODO: We should refactor the parser to use the same types and structures for host bindings as
// with ordinary components. This would allow us to share a lot more ingestion code.
export function ingestDomProperty(
@@ -60,14 +60,12 @@ export function resolveSanitizers(job: CompilationJob): void {
let sanitizerFn: o.ExternalReference | null = null;
if (
Array.isArray(op.securityContext) &&
op.securityContext.length === 2 &&
op.securityContext.includes(SecurityContext.URL) &&
op.securityContext.includes(SecurityContext.RESOURCE_URL)
hasCompositeUrlSecurityContext(op.securityContext)
) {
// When the host element isn't known, some URL attributes (such as "src" and "href") may
// be part of multiple different security contexts. In this case we use special
// sanitization function and select the actual sanitizer at runtime based on a tag name
// that is provided while invoking sanitization function.
// When the host element isn't known, attributes such as `href`, `src`, `data`,
// `action`, and `codebase` may be part of multiple security contexts. In this case we
// use a special sanitization function and select the actual behavior at runtime based
// on the concrete host element.
sanitizerFn = Identifiers.sanitizeUrlOrResourceUrl;
} else {
sanitizerFn = sanitizerFns.get(getOnlySecurityContext(op.securityContext)) ?? null;
@@ -81,21 +79,56 @@ export function resolveSanitizers(job: CompilationJob): void {
}
}
function hasCompositeUrlSecurityContext(securityContext: SecurityContext[]): boolean {
let hasUrlContext = false;
let hasResourceUrlContext = false;
let hasNoneContext = false;
for (const context of securityContext) {
switch (context) {
case SecurityContext.URL:
hasUrlContext = true;
break;
case SecurityContext.RESOURCE_URL:
hasResourceUrlContext = true;
break;
case SecurityContext.NONE:
hasNoneContext = true;
break;
default:
return false;
}
}
return (
((hasUrlContext || hasResourceUrlContext) && hasNoneContext) ||
(hasUrlContext && hasResourceUrlContext)
);
}
/**
* Asserts that there is only a single security context and returns it.
* Asserts that there is only a single non-NONE security context and returns it.
*/
function getOnlySecurityContext(
securityContext: SecurityContext | SecurityContext[],
): SecurityContext {
if (Array.isArray(securityContext)) {
if (securityContext.length > 1) {
// TODO: What should we do here? TDB just took the first one, but this feels like something we
// would want to know about and create a special case for like we did for Url/ResourceUrl. My
// guess is that, outside of the Url/ResourceUrl case, this never actually happens. If there
// do turn out to be other cases, throwing an error until we can address it feels safer.
throw Error(`AssertionError: Ambiguous security context`);
}
return securityContext[0] || SecurityContext.NONE;
if (!Array.isArray(securityContext)) {
return securityContext;
}
return securityContext;
if (securityContext.length < 2) {
return securityContext[0] ?? SecurityContext.NONE;
}
const nonNoneSecurityContexts = securityContext.filter(
(context) => context !== SecurityContext.NONE,
);
if (nonNoneSecurityContexts.length > 1) {
// TODO: What should we do here? TDB just took the first one, but this feels like something we
// would want to know about and create a special case for like we did for Url/ResourceUrl. My
// guess is that, outside of the Url/ResourceUrl case, this never actually happens. If there
// do turn out to be other cases, throwing an error until we can address it feels safer.
throw Error(`AssertionError: Ambiguous security context`);
}
return nonNoneSecurityContexts[0] ?? SecurityContext.NONE;
}
+10 -1
View File
@@ -104,11 +104,20 @@ export function getLocalePluralCase(locale: string): (value: number) => number {
*/
export function getLocaleData(normalizedLocale: string): any {
if (!(normalizedLocale in LOCALE_DATA)) {
LOCALE_DATA[normalizedLocale] =
const globalLocaleData =
global.ng &&
global.ng.common &&
global.ng.common.locales &&
global.ng.common.locales[normalizedLocale];
// Only cache global locale data when an entry is actually found, to avoid
// caching missing lookups. In SSR this cache is process-wide across requests,
// so caching `undefined` would retain attacker-controlled locale identifiers
// indefinitely. It would also make the `in` check above short-circuit on
// subsequent lookups and skip the global fallback.
if (globalLocaleData !== undefined) {
LOCALE_DATA[normalizedLocale] = globalLocaleData;
}
return globalLocaleData;
}
return LOCALE_DATA[normalizedLocale];
}
+13 -2
View File
@@ -25,6 +25,7 @@ import {ComponentFactoryResolver as AbstractComponentFactoryResolver} from '../l
import {createElementRef, ElementRef} from '../linker/element_ref';
import {NgModuleRef} from '../linker/ng_module_factory';
import {RendererFactory2} from '../render/api';
import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from '../sanitization/dom_security_schema';
import {Sanitizer} from '../sanitization/sanitizer';
import {assertComponentType} from './assert';
@@ -47,6 +48,7 @@ import {
TElementContainerNode,
TElementNode,
TNode,
TNodeName,
TNodeType,
} from './interfaces/node';
import {RElement, RNode} from './interfaces/renderer_dom';
@@ -60,7 +62,6 @@ import {
TVIEW,
TViewType,
} from './interfaces/view';
import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from './namespaces';
import {retrieveHydrationInfo} from '../hydration/utils';
import {ChainedInjector} from './chained_injector';
@@ -197,6 +198,15 @@ function createHostElement(componentDef: ComponentDef<unknown>, renderer: Render
return createElementNode(renderer, tagName, namespace);
}
function assertNotScriptHostElement(tagName: string | null | undefined): void {
if (tagName?.toLowerCase() === 'script') {
throw new RuntimeError(
RuntimeErrorCode.UNSAFE_VALUE_IN_SCRIPT,
ngDevMode && `"<script>" tag is not allowed as a component host element.`,
);
}
}
/**
* Infers the tag name that should be used for a component based on its definition.
* @param componentDef Definition for which to resolve the tag name.
@@ -318,6 +328,7 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
const hostElement = rootSelectorOrNode
? locateHostElement(hostRenderer, rootSelectorOrNode, cmpDef.encapsulation, rootViewInjector)
: createHostElement(cmpDef, hostRenderer);
assertNotScriptHostElement(hostElement?.tagName);
const hasInputBindings =
componentBindings?.some(isInputBinding) ||
directives?.some((d) => typeof d !== 'function' && d.bindings.some(isInputBinding));
@@ -352,7 +363,7 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
HEADER_OFFSET,
rootLView,
TNodeType.Element,
'#host',
TNodeName.DynamicHost,
() => rootTView.directiveRegistry,
true,
0,
+8 -26
View File
@@ -72,8 +72,9 @@ import {
setTNodeInsertBeforeIndex,
} from './i18n_util';
import {createTNodeAtIndex} from '../tnode_manipulation';
import {MATH_ML_NAMESPACE, NAMESPACE_URIS, SVG_NAMESPACE} from '../namespaces';
import {splitNsName} from '../util/tags';
import {allocExpando} from '../view/construction';
import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from '../namespaces';
const BINDING_REGEXP = /<2F>(\d+):?\d*<2A>/gi;
const ICU_REGEXP = /({\s*<2A>\d+:?\d*<2A>\s*,\s*\S{6}\s*,[\s\S]*})/gi;
@@ -815,13 +816,10 @@ function walkIcuTree(
const attr = elAttrs.item(i)!;
const lowerAttrName = attr.name.toLowerCase();
const hasBinding = !!attr.value.match(BINDING_REGEXP);
const elementNS = element.namespaceURI;
const tagNameWithNamespace =
elementNS === 'http://www.w3.org/2000/svg'
? `:svg:${tagName}`
: elementNS === 'http://www.w3.org/1998/Math/MathML'
? `:math:${tagName}`
: tagName;
const namespaceUri = element.namespaceURI;
const namespace = namespaceUri && NAMESPACE_URIS[namespaceUri];
const tagNameWithNamespace = namespace ? `:${namespace}:${tagName}` : tagName;
if (hasBinding) {
if (VALID_ATTRS.hasOwnProperty(lowerAttrName)) {
generateBindingUpdateOpCodes(
@@ -985,24 +983,6 @@ function addCreateAttribute(
create.push((newIndex << IcuCreateOpCode.SHIFT_REF) | IcuCreateOpCode.Attr, attrName, attrValue);
}
function splitNsName(elementName: string, fatal: boolean = true): [string | null, string] {
if (elementName[0] != ':') {
return [null, elementName];
}
const colonIndex = elementName.indexOf(':', 1);
if (colonIndex === -1) {
if (fatal) {
throw new Error(`Unsupported format "${elementName}" expecting ":namespace:name"`);
} else {
return [null, elementName];
}
}
return [elementName.slice(1, colonIndex), elementName.slice(colonIndex + 1)];
}
function normalizeTagName(tagName: string): string {
const tagNameLower = tagName.toLowerCase();
const [ns, name] = splitNsName(tagNameLower, false);
@@ -1013,9 +993,11 @@ function normalizeTagName(tagName: string): string {
function i18nResolveSanitizer(attrName: string, tagName?: string): SanitizerFn | null {
const lowerAttrName = attrName.toLowerCase();
const lowerTagName = tagName ? normalizeTagName(tagName) : '*';
const [namespace] = splitNsName(lowerTagName, false);
const schema = SECURITY_SCHEMA();
const schemaContext =
schema[`${lowerTagName}|${lowerAttrName}`] ||
(namespace ? schema[`:${namespace}:*|${lowerAttrName}`] : undefined) ||
schema[`*|${lowerAttrName}`] ||
SecurityContext.NONE;
@@ -21,7 +21,6 @@ import {stringify} from '../../util/stringify';
import {assertFirstCreatePass, assertHasParent, assertLView} from '../assert';
import {attachPatchData} from '../context_discovery';
import {getNodeInjectable, getOrCreateNodeInjectorForNode} from '../di';
import {RuntimeError, RuntimeErrorCode} from '../../errors';
import {throwMultipleComponentError} from '../errors';
import {ComponentDef, ComponentTemplate, DirectiveDef, RenderFlags} from '../interfaces/definition';
import {
@@ -181,12 +180,6 @@ export function locateHostElement(
encapsulation === ViewEncapsulation.ShadowDom ||
encapsulation === ViewEncapsulation.ExperimentalIsolatedShadowDom;
const rootElement = renderer.selectRootElement(elementOrSelector, preserveContent);
if (rootElement.tagName.toLowerCase() === 'script') {
throw new RuntimeError(
RuntimeErrorCode.UNSAFE_VALUE_IN_SCRIPT,
ngDevMode && `"<script>" tag is not allowed as a component host element.`,
);
}
applyRootElementTransform(rootElement as HTMLElement);
return rootElement;
}
@@ -15,6 +15,14 @@ import {CssSelector} from './projection';
import {RNode} from './renderer_dom';
import type {LView, TView} from './view';
/**
* Internal tag name used for a root host `TNode` when Angular creates a component against an
* existing host element. The concrete DOM tag is resolved from the native element at runtime.
*/
export const enum TNodeName {
DynamicHost = '#host',
}
/**
* TNodeType corresponds to the {@link TNode} `type` property.
*
+5
View File
@@ -8,3 +8,8 @@
export const SVG_NAMESPACE = 'svg';
export const MATH_ML_NAMESPACE = 'math';
export const NAMESPACE_URIS: Record<string, string> = {
'http://www.w3.org/2000/svg': SVG_NAMESPACE,
'http://www.w3.org/1998/Math/MathML': MATH_ML_NAMESPACE,
};
+1 -1
View File
@@ -7,6 +7,7 @@
*/
import {InternalInjectFlags} from '../di/interface/injector';
import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from '../sanitization/dom_security_schema';
import {
assertDefined,
assertEqual,
@@ -31,7 +32,6 @@ import {
TView,
TViewType,
} from './interfaces/view';
import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from './namespaces';
import {getTNode, walkUpViews} from './util/view_utils';
/**
+32
View File
@@ -0,0 +1,32 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
/**
* Splits an element name into its namespace and local name.
*
* @param elementName The element name to split, in the format ":namespace:name".
* @param fatal If true, throws an error if the element name is not in the correct format.
* @returns A tuple containing the namespace and local name.
*/
export function splitNsName(elementName: string, fatal: boolean = true): [string | null, string] {
if (elementName[0] != ':') {
return [null, elementName];
}
const colonIndex = elementName.indexOf(':', 1);
if (colonIndex === -1) {
if (fatal) {
throw new Error(`Unsupported format "${elementName}" expecting ":namespace:name"`);
} else {
return [null, elementName];
}
}
return [elementName.slice(1, colonIndex), elementName.slice(colonIndex + 1)];
}
@@ -39,8 +39,8 @@ export enum SecurityContext {
* Map from tagName|propertyName to SecurityContext. Properties applying to all tags use '*'.
*/
let _SECURITY_SCHEMA!: {[k: string]: SecurityContext};
const SVG_NAMESPACE = 'svg';
const MATH_ML_NAMESPACE = 'math';
export const SVG_NAMESPACE = 'svg';
export const MATH_ML_NAMESPACE = 'math';
/**
* @remarks Keep is a copy of DOM Security Schema.
@@ -71,6 +71,7 @@ export function SECURITY_SCHEMA(): {[k: string]: SecurityContext} {
registerContext(SecurityContext.URL, MATH_ML_NAMESPACE, [
// MathML namespace
// https://crsrc.org/c/third_party/blink/renderer/core/sanitizer/sanitizer.cc;l=753-768;drc=b3eb16372dcd3317d65e9e0265015e322494edcd;bpv=1;bpt=1
['*', ['href', 'xlink:href']],
['annotation', ['href', 'xlink:href']],
['annotation-xml', ['href', 'xlink:href']],
['maction', ['href', 'xlink:href']],
@@ -160,8 +161,7 @@ function registerContext(
specs: readonly [tagName: string, attributeNames: readonly string[]][],
): void {
for (const [element, attributeNames] of specs) {
let tagName =
namespace && element !== '*' && element !== 'unknown' ? `:${namespace}:${element}` : element;
let tagName = namespace && element !== 'unknown' ? `:${namespace}:${element}` : element;
tagName = tagName.toLowerCase();
for (const attr of attributeNames) {
@@ -169,3 +169,28 @@ function registerContext(
}
}
}
export function checkSecurityContext(
tagName: string,
propName: string,
namespace?: string | null,
): SecurityContext {
const schema = SECURITY_SCHEMA();
const normalizedTagName = tagName.toLowerCase();
const normalizedPropName = propName.toLowerCase();
const namespacedContext =
namespace && normalizedTagName !== '*' && normalizedTagName !== 'unknown'
? schema[`:${namespace}:${normalizedTagName}|${normalizedPropName}`]
: undefined;
const namespacedWildcardContext = namespace
? schema[`:${namespace}:*|${normalizedPropName}`]
: undefined;
return (
namespacedContext ??
namespacedWildcardContext ??
schema[`${normalizedTagName}|${normalizedPropName}`] ??
schema[`*|${normalizedPropName}`] ??
SecurityContext.NONE
);
}
+99 -89
View File
@@ -10,12 +10,13 @@ import {XSS_SECURITY_URL} from '../error_details_base_url';
import {RuntimeError, RuntimeErrorCode} from '../errors';
import {getTemplateLocationDetails} from '../render3/instructions/element_validation';
import {getDocument} from '../render3/interfaces/document';
import {TNode, TNodeType} from '../render3/interfaces/node';
import {TNode, TNodeName, TNodeType} from '../render3/interfaces/node';
import {RElement} from '../render3/interfaces/renderer_dom';
import {ENVIRONMENT} from '../render3/interfaces/view';
import {getLView, getSelectedIndex, getSelectedTNode} from '../render3/state';
import {renderStringify} from '../render3/util/stringify_utils';
import {getNativeByTNode} from '../render3/util/view_utils';
import {NAMESPACE_URIS} from '../render3/namespaces';
import {TrustedHTML, TrustedScript, TrustedScriptURL} from '../util/security/trusted_type_defs';
import {trustedHTMLFromString, trustedScriptURLFromString} from '../util/security/trusted_types';
import {
@@ -28,8 +29,9 @@ import {allowSanitizationBypassAndThrow, BypassType, unwrapSafeValue} from './by
import {_sanitizeHtml} from './html_sanitizer';
import {enforceIframeSecurity} from './iframe_attrs_validation';
import {Sanitizer} from './sanitizer';
import {SecurityContext} from './dom_security_schema';
import {checkSecurityContext, SecurityContext, SVG_NAMESPACE} from './dom_security_schema';
import {_sanitizeUrl} from './url_sanitizer';
import {splitNsName} from '../render3/util/tags';
/**
* An `html` sanitizer which converts untrusted `html` **string** into trusted string by removing
@@ -46,7 +48,19 @@ import {_sanitizeUrl} from './url_sanitizer';
*
* @codeGenApi
*/
export function ɵɵsanitizeHtml(unsafeHtml: any): TrustedHTML | string {
export function ɵɵsanitizeHtml(
unsafeHtml: any,
tagName?: string,
propName?: string,
): TrustedHTML | string {
if (
tagName !== undefined &&
propName !== undefined &&
getSecurityContext(tagName, propName) !== SecurityContext.HTML
) {
return unsafeHtml;
}
const sanitizer = getSanitizer();
if (sanitizer) {
return trustedHTMLFromStringBypass(sanitizer.sanitize(SecurityContext.HTML, unsafeHtml) || '');
@@ -213,29 +227,22 @@ export function ɵɵtrustConstantResourceUrl(url: TemplateStringsArray): Trusted
return trustedScriptURLFromString(url[0]);
}
// Define sets outside the function for O(1) lookups and memory efficiency
const RESOURCE_MAP: Record<string, Record<string, true | undefined> | undefined> = {
'embed': {'src': true},
'frame': {'src': true},
'iframe': {'src': true},
'media': {'src': true},
'base': {'href': true},
'link': {'href': true},
'object': {'data': true, 'codebase': true},
};
/**
* Detects which sanitizer to use for URL property, based on tag name and prop name.
*
* The rules are based on the RESOURCE_URL context config from
* The rules are based on the URL and RESOURCE_URL context config from
* `packages/compiler/src/schema/dom_security_schema.ts`.
* If tag and prop names don't match Resource URL schema, use URL sanitizer.
* If tag and prop names don't match URL or Resource URL schema, no sanitizer is required.
*/
export function getUrlSanitizer(tag: string, prop: string) {
const isResource = RESOURCE_MAP[tag.toLowerCase()]?.[prop.toLowerCase()] === true;
return isResource ? ɵɵsanitizeResourceUrl : ɵɵsanitizeUrl;
switch (getSecurityContext(tag, prop)) {
case SecurityContext.RESOURCE_URL:
return ɵɵsanitizeResourceUrl;
case SecurityContext.URL:
return ɵɵsanitizeUrl;
default:
return null;
}
}
/**
@@ -253,8 +260,8 @@ export function getUrlSanitizer(tag: string, prop: string) {
*
* @codeGenApi
*/
export function ɵɵsanitizeUrlOrResourceUrl(unsafeUrl: any, tag: string, prop: string): any {
return getUrlSanitizer(tag, prop)(unsafeUrl);
export function ɵɵsanitizeUrlOrResourceUrl(unsafeUrl: any, tag: string, prop: string) {
return getUrlSanitizer(tag, prop)?.(unsafeUrl) ?? unsafeUrl;
}
export function validateAgainstEventProperties(name: string) {
@@ -273,37 +280,56 @@ function getSanitizer(): Sanitizer | null {
return lView && lView[ENVIRONMENT].sanitizer;
}
function getSecurityContext(tagName: string, propName: string): SecurityContext {
const [namespace, resolvedTagName] = resolveElement(tagName);
return checkSecurityContext(resolvedTagName, propName, namespace);
}
function resolveElement(tagName: string): [namespace: string | null | undefined, tagName: string] {
tagName = tagName.toLowerCase();
const splitResult = splitNsName(tagName, false);
if (splitResult[0]) {
return splitResult;
}
const index = getSelectedIndex();
const tNode = index === -1 ? null : getSelectedTNode();
let namespace = tNode?.namespace;
if (tagName === TNodeName.DynamicHost && tNode?.type === TNodeType.Element) {
const element = getNativeByTNode(tNode, getLView()) as RElement;
if (element.tagName) {
tagName = element.tagName.toLowerCase();
}
if (namespace == null) {
const namespaceURI = (element as RElement & {namespaceURI?: string | null}).namespaceURI;
namespace = namespaceURI && NAMESPACE_URIS[namespaceURI];
}
}
return [namespace, tagName];
}
/**
* Set of attributes that are sensitive and should be sanitized.
*/
const SECURITY_SENSITIVE_ATTRIBUTE_NAMES: ReadonlySet<string> = new Set(['href', 'xlink:href']);
const SVG_ANIMATION_ATTRIBUTE_NAME_CANDIDATES = ['attributeName', 'attributename'] as const;
/**
* @remarks Keep this in sync with DOM Security Schema.
* @see [SECURITY_SCHEMA](../../../compiler/src/schema/dom_security_schema.ts)
*/
const SECURITY_SENSITIVE_ELEMENTS: Record<
const SVG_ANIMATION_SENSITIVE_STATIC_VALUES: Record<
string,
Record<string, true | undefined | ReadonlySet<string>> | undefined
Record<string, ReadonlySet<string>> | undefined
> = {
'iframe': {
'sandbox': true,
'allow': true,
'allowfullscreen': true,
'referrerpolicy': true,
'csp': true,
'fetchpriority': true,
},
':svg:animate': {
'attributename': true,
'animate': {
'to': SECURITY_SENSITIVE_ATTRIBUTE_NAMES,
'values': SECURITY_SENSITIVE_ATTRIBUTE_NAMES,
'from': SECURITY_SENSITIVE_ATTRIBUTE_NAMES,
},
':svg:set': {'attributename': true, 'to': SECURITY_SENSITIVE_ATTRIBUTE_NAMES},
':svg:animatemotion': {'attributename': true},
':svg:animatetransform': {'attributename': true},
'set': {'to': SECURITY_SENSITIVE_ATTRIBUTE_NAMES},
};
/**
@@ -314,78 +340,58 @@ const SECURITY_SENSITIVE_ELEMENTS: Record<
* @param attributeName The name of the attribute.
*/
export function ɵɵvalidateAttribute<T = any>(value: T, tagName: string, attributeName: string): T {
const lowerCaseTagName = tagName.toLowerCase();
const lowerCaseAttrName = attributeName.toLowerCase();
const index = getSelectedIndex();
const tNode: TNode | null = index === -1 ? null : getSelectedTNode();
if (tNode && tNode.type !== TNodeType.Element) {
return value;
}
// Leverage tNode.namespace if active, otherwise check both namespaced and base variants.
const fullTagName =
lowerCaseTagName[0] !== ':' && tNode?.namespace
? `:${tNode.namespace}:${lowerCaseTagName}`
: lowerCaseTagName;
const [namespace, resolvedTagName] = resolveElement(tagName);
const securityContext = checkSecurityContext(resolvedTagName, attributeName, namespace);
const validationConfig = SECURITY_SENSITIVE_ELEMENTS[fullTagName]?.[lowerCaseAttrName];
if (!validationConfig) {
if (securityContext !== SecurityContext.ATTRIBUTE_NO_BINDING) {
return value;
}
const lView = getLView();
if (tNode && lowerCaseTagName === 'iframe') {
const element = getNativeByTNode(tNode, lView) as RElement;
enforceIframeSecurity(element as HTMLIFrameElement);
}
if (tNode) {
if (resolvedTagName === 'iframe') {
const element = getNativeByTNode(tNode, lView) as RElement;
enforceIframeSecurity(element as HTMLIFrameElement);
} else if (namespace === SVG_NAMESPACE) {
const config =
SVG_ANIMATION_SENSITIVE_STATIC_VALUES[resolvedTagName]?.[attributeName.toLowerCase()];
if (config) {
const element = getNativeByTNode(tNode, lView) as SVGAnimateElement;
const attributeNameValue = getSecuritySensitiveSVGAnimationAttributeName(element, config);
const displayTagName = tagName[0] === ':' ? tagName.split(':').pop()! : tagName;
if (attributeNameValue) {
const errorMessage =
ngDevMode &&
`Angular has detected that the \`${attributeName}\` was applied ` +
`as a binding to the <${resolvedTagName}> element${getTemplateLocationDetails(lView)}. ` +
`For security reasons, the \`${attributeName}\` can be set on the <${resolvedTagName}> element ` +
`as a static attribute only when the "attributeName" is set to \'${attributeNameValue}\'. \n` +
`To fix this, switch the \`${attributeNameValue}\` binding to a static attribute ` +
`in a template or in host bindings section.`;
if (typeof validationConfig !== 'boolean') {
if (!tNode) {
const errorMessage =
ngDevMode &&
`Angular has detected that the \`${attributeName}\` was applied ` +
`as a binding to the <${tagName}> element. ` +
`For security reasons, the \`${attributeName}\` can be set on the <${tagName}> element ` +
`as a static attribute only. \n` +
`To fix this, switch the \`${attributeName}\` binding to a static attribute ` +
`in a template or in host bindings section.`;
throw new RuntimeError(RuntimeErrorCode.UNSAFE_ATTRIBUTE_BINDING, errorMessage);
throw new RuntimeError(RuntimeErrorCode.UNSAFE_ATTRIBUTE_BINDING, errorMessage);
}
return value;
}
}
const element = getNativeByTNode(tNode, lView) as SVGAnimateElement;
const attributeNameValue = getSecuritySensitiveSVGAnimationAttributeName(
element,
validationConfig,
);
if (attributeNameValue) {
const errorMessage =
ngDevMode &&
`Angular has detected that the \`${attributeName}\` was applied ` +
`as a binding to the <${displayTagName}> element${getTemplateLocationDetails(lView)}. ` +
`For security reasons, the \`${attributeName}\` can be set on the <${displayTagName}> element ` +
`as a static attribute only when the "attributeName" is set to \'${attributeNameValue}\'. \n` +
`To fix this, switch the \`${attributeNameValue}\` binding to a static attribute ` +
`in a template or in host bindings section.`;
throw new RuntimeError(RuntimeErrorCode.UNSAFE_ATTRIBUTE_BINDING, errorMessage);
}
return value;
}
const errorMessage =
ngDevMode &&
`Angular has detected that the \`${attributeName}\` was applied ` +
`as a binding to the <${displayTagName}> element${tNode ? getTemplateLocationDetails(lView) : ''}. ` +
`For security reasons, the \`${attributeName}\` can be set on the <${displayTagName}> element ` +
`as a binding to the <${resolvedTagName}> element${tNode ? getTemplateLocationDetails(lView) : ''}. ` +
`For security reasons, the \`${attributeName}\` can be set on the <${resolvedTagName}> element ` +
`as a static attribute only. \n` +
`To fix this, switch the \`${attributeName}\` binding to a static attribute ` +
`in a template or in host bindings section.`;
throw new RuntimeError(RuntimeErrorCode.UNSAFE_ATTRIBUTE_BINDING, errorMessage);
}
@@ -393,7 +399,11 @@ function getSecuritySensitiveSVGAnimationAttributeName(
element: SVGAnimateElement,
validationConfig: ReadonlySet<string>,
): string | null {
for (const attributeName of SVG_ANIMATION_ATTRIBUTE_NAME_CANDIDATES) {
for (const attributeName of element.getAttributeNames()) {
if (attributeName.toLowerCase() !== 'attributename') {
continue;
}
const attributeNameValue = element.getAttribute(attributeName);
if (attributeNameValue !== null && validationConfig.has(attributeNameValue.toLowerCase())) {
return attributeNameValue;
+406 -1
View File
@@ -9,12 +9,16 @@
import {NgIf} from '@angular/common';
import {DomSanitizer} from '@angular/platform-browser';
import {
ApplicationRef,
ChangeDetectionStrategy,
Component,
ComponentRef,
createComponent,
Directive,
EnvironmentInjector,
inject,
inputBinding,
Input,
provideZoneChangeDetection,
TemplateRef,
Type,
@@ -25,6 +29,9 @@ import {RuntimeErrorCode} from '../../src/errors';
import {global} from '../../src/util/global';
import {ComponentFixture, TestBed} from '../../testing';
const SVG_NAMESPACE_URI = 'http://www.w3.org/2000/svg';
const MATH_ML_NAMESPACE_URI = 'http://www.w3.org/1998/Math/MathML';
describe('comment node text escaping', () => {
// see: https://html.spec.whatwg.org/multipage/syntax.html#comments
[
@@ -793,7 +800,405 @@ describe('innerHTML processing', () => {
});
});
describe('host binding sanitization', () => {
const HOST_BINDING_URL = 'http://server/asset';
const HOST_BINDING_UNSAFE_URL = 'javascript:custom-data';
const UNSAFE_HTML = `<script>evil</script>` + '<p>safe</p>';
const SANITIZED_HTML = '<p>safe</p>';
const resourceUrlError = /NG0904: unsafe value used in a resource URL context.*/;
async function expectHostBinding(options: {
tagName: string;
attrName: string;
value: string;
expected?: string;
expectedError?: RegExp;
namespace?: string;
componentSelector?: string;
}): Promise<void> {
// Avoid duplicate selector generation.
const randomIdentifier = Math.floor(Math.random() * 100);
const {
tagName,
attrName,
value,
expected,
expectedError,
namespace,
componentSelector = `dynamic-host-${randomIdentifier}`,
} = options;
@Directive({
selector: `[safe-data-carrier-${randomIdentifier}]`,
host: {[`[attr.${attrName}]`]: 'val'},
})
class CarrierDirective {
val = value;
}
@Component({
selector: componentSelector,
template: '',
})
class DynamicComponent {}
const hostElement = namespace
? document.createElementNS(namespace, tagName)
: document.createElement(tagName);
let componentRef: ComponentRef<DynamicComponent> | undefined;
@Component({
template: '',
})
class AppHost {
private appRef = inject(ApplicationRef);
private environmentInjector = inject(EnvironmentInjector);
constructor() {
componentRef = createComponent(DynamicComponent, {
hostElement,
environmentInjector: this.environmentInjector,
directives: [CarrierDirective],
});
this.appRef.attachView(componentRef.hostView);
}
}
const fixture = TestBed.createComponent(AppHost);
try {
if (expectedError) {
await expectAsync(fixture.whenStable()).toBeRejectedWithError(expectedError);
} else {
await fixture.whenStable();
expect(hostElement.getAttribute(attrName)).toBe(expected ?? null);
}
} finally {
componentRef?.destroy();
}
}
it('should not sanitize resource URL attribute names on non-resource concrete hosts', async () => {
await expectHostBinding({
tagName: 'div',
attrName: 'data',
value: HOST_BINDING_URL,
expected: HOST_BINDING_URL,
});
await expectHostBinding({
tagName: 'div',
attrName: 'data',
value: HOST_BINDING_UNSAFE_URL,
expected: HOST_BINDING_UNSAFE_URL,
});
});
it('should sanitize href host bindings on SVG links', async () => {
await expectHostBinding({
tagName: 'a',
attrName: 'href',
value: HOST_BINDING_UNSAFE_URL,
expected: `unsafe:${HOST_BINDING_UNSAFE_URL}`,
namespace: SVG_NAMESPACE_URI,
});
});
it('should not sanitize href host bindings on non-link SVG elements', async () => {
await expectHostBinding({
tagName: 'rect',
attrName: 'href',
value: HOST_BINDING_UNSAFE_URL,
expected: HOST_BINDING_UNSAFE_URL,
namespace: SVG_NAMESPACE_URI,
});
});
it('should sanitize xlink:href host bindings on SVG links', async () => {
await expectHostBinding({
tagName: 'a',
attrName: 'xlink:href',
value: HOST_BINDING_UNSAFE_URL,
expected: `unsafe:${HOST_BINDING_UNSAFE_URL}`,
namespace: SVG_NAMESPACE_URI,
});
});
it('should sanitize href host bindings on MathML elements', async () => {
await expectHostBinding({
tagName: 'mi',
attrName: 'href',
value: HOST_BINDING_UNSAFE_URL,
expected: `unsafe:${HOST_BINDING_UNSAFE_URL}`,
namespace: MATH_ML_NAMESPACE_URI,
});
});
it('should sanitize href host bindings on dynamic SVG hosts as URLs', async () => {
await expectHostBinding({
tagName: 'a',
attrName: 'href',
value: HOST_BINDING_UNSAFE_URL,
expected: `unsafe:${HOST_BINDING_UNSAFE_URL}`,
namespace: SVG_NAMESPACE_URI,
});
});
it('should sanitize href host bindings on dynamic MathML hosts as URLs', async () => {
await expectHostBinding({
tagName: 'base',
attrName: 'href',
value: HOST_BINDING_UNSAFE_URL,
expected: `unsafe:${HOST_BINDING_UNSAFE_URL}`,
namespace: MATH_ML_NAMESPACE_URI,
});
});
it('should sanitize a dynamic directive host binding against the concrete host element', async () => {
@Component({
selector: 'iframe',
template: '',
})
class DynamicIframeHostComponent {}
@Directive({
selector: 'safe-srcdoc-carrier',
host: {'[attr.srcdoc]': 'srcdoc'},
})
class SafeSrcdocCarrierDirective {
@Input() srcdoc = '';
}
@Component({
template: '',
imports: [DynamicIframeHostComponent],
})
class App {
componentRef: ComponentRef<DynamicIframeHostComponent>;
private viewContainerRef = inject(ViewContainerRef);
private environmentInjector = inject(EnvironmentInjector);
constructor() {
this.componentRef = this.viewContainerRef.createComponent(DynamicIframeHostComponent, {
environmentInjector: this.environmentInjector,
directives: [
{
type: SafeSrcdocCarrierDirective,
bindings: [inputBinding('srcdoc', () => UNSAFE_HTML)],
},
],
});
}
}
const fixture = TestBed.createComponent(App);
await fixture.whenStable();
const iframe = fixture.componentInstance.componentRef.location
.nativeElement as HTMLIFrameElement;
expect(iframe.getAttribute('srcdoc')).toBe(SANITIZED_HTML);
expect(iframe.getAttribute('srcdoc')).not.toContain('<script>');
});
it('should not sanitize iframe-only host bindings on non-iframe concrete hosts', async () => {
await expectHostBinding({
tagName: 'div',
attrName: 'srcdoc',
value: UNSAFE_HTML,
expected: UNSAFE_HTML,
});
});
it('should not sanitize form-only URL host bindings on non-form concrete hosts', async () => {
await expectHostBinding({
tagName: 'div',
attrName: 'action',
value: HOST_BINDING_URL,
expected: HOST_BINDING_URL,
});
await expectHostBinding({
tagName: 'div',
attrName: 'action',
value: HOST_BINDING_UNSAFE_URL,
expected: HOST_BINDING_UNSAFE_URL,
});
});
it('should sanitize form-only URL host bindings on form concrete hosts', async () => {
await expectHostBinding({
tagName: 'form',
attrName: 'action',
value: HOST_BINDING_URL,
expected: HOST_BINDING_URL,
});
await expectHostBinding({
tagName: 'form',
attrName: 'action',
value: HOST_BINDING_UNSAFE_URL,
expected: `unsafe:${HOST_BINDING_UNSAFE_URL}`,
});
});
it('should sanitize a host directive host binding against the concrete host element', async () => {
@Directive({
selector: 'safe-srcdoc-host',
host: {'[attr.srcdoc]': 'srcdoc'},
})
class SafeSrcdocHostDirective {
srcdoc = UNSAFE_HTML;
}
@Directive({
selector: '[safe-srcdoc-host-carrier]',
hostDirectives: [SafeSrcdocHostDirective],
})
class SafeSrcdocHostCarrierDirective {}
@Component({
template: '<iframe safe-srcdoc-host-carrier></iframe>',
imports: [SafeSrcdocHostCarrierDirective],
})
class App {}
const fixture = TestBed.createComponent(App);
await fixture.whenStable();
const iframe = fixture.nativeElement.querySelector('iframe') as HTMLIFrameElement;
expect(iframe.getAttribute('srcdoc')).toBe(SANITIZED_HTML);
expect(iframe.getAttribute('srcdoc')).not.toContain('<script>');
});
it('should sanitize an inherited host binding against the concrete host element', async () => {
@Directive({
selector: 'base-srcdoc-carrier',
host: {'[attr.srcdoc]': 'srcdoc'},
})
class BaseSrcdocCarrierDirective {
srcdoc = UNSAFE_HTML;
}
@Directive({
selector: '[derived-srcdoc-carrier]',
})
class DerivedSrcdocCarrierDirective extends BaseSrcdocCarrierDirective {}
@Component({
template: '<iframe derived-srcdoc-carrier></iframe>',
imports: [DerivedSrcdocCarrierDirective],
})
class App {}
const fixture = TestBed.createComponent(App);
await fixture.whenStable();
const iframe = fixture.nativeElement.querySelector('iframe') as HTMLIFrameElement;
expect(iframe.getAttribute('srcdoc')).toBe(SANITIZED_HTML);
expect(iframe.getAttribute('srcdoc')).not.toContain('<script>');
});
it('should reject dynamic directive host bindings against concrete resource URL sinks', async () => {
@Component({
selector: 'iframe',
template: '',
})
class DynamicIframeHostComponent {}
@Directive({
selector: 'src-carrier',
host: {'[attr.src]': 'src'},
})
class SrcCarrierDirective {
@Input() src = '';
}
@Component({
template: '',
imports: [DynamicIframeHostComponent],
})
class App {
private viewContainerRef = inject(ViewContainerRef);
private environmentInjector = inject(EnvironmentInjector);
constructor() {
this.viewContainerRef.createComponent(DynamicIframeHostComponent, {
environmentInjector: this.environmentInjector,
directives: [
{
type: SrcCarrierDirective,
bindings: [inputBinding('src', () => HOST_BINDING_URL)],
},
],
});
}
}
const fixture = TestBed.createComponent(App);
await expectAsync(fixture.whenStable()).toBeRejectedWithError(resourceUrlError);
});
it('should reject security-sensitive attribute host bindings on concrete dynamic iframe hosts', async () => {
await expectHostBinding({
tagName: 'iframe',
attrName: 'sandbox',
value: '',
expectedError:
/NG0910: Angular has detected that the `sandbox` was applied as a binding to the <iframe>/,
});
});
it('should reject security-sensitive attribute host bindings on concrete dynamic SVG animation hosts', async () => {
await expectHostBinding({
tagName: 'animate',
attrName: 'attributeName',
value: 'href',
expectedError:
/NG0910: Angular has detected that the `attributeName` was applied as a binding to the <animate>/,
namespace: SVG_NAMESPACE_URI,
});
});
it('should sanitize pure :not selector host bindings against a concrete hostElement', async () => {
await expectHostBinding({
tagName: 'iframe',
attrName: 'srcdoc',
value: UNSAFE_HTML,
expected: SANITIZED_HTML,
componentSelector: ':not(iframe)',
});
});
it('should reject object data host bindings against concrete resource URL sinks', async () => {
await expectHostBinding({
tagName: 'object',
attrName: 'data',
value: HOST_BINDING_URL,
expectedError: resourceUrlError,
});
await expectHostBinding({
tagName: 'object',
attrName: 'data',
value: HOST_BINDING_UNSAFE_URL,
expectedError: resourceUrlError,
});
});
});
describe('Component host element validation', () => {
it('should throw an error when dynamically creating a component with a script selector', () => {
@Component({
selector: 'script',
template: '',
})
class ScriptHost {}
const environmentInjector = TestBed.inject(EnvironmentInjector);
expect(() => {
createComponent(ScriptHost, {environmentInjector});
}).toThrowError(/"<script>" tag is not allowed as a component host element/);
});
it('should throw an error when dynamically mounting a component onto a script tag', () => {
@Component({
selector: 'my-sink',
@@ -824,7 +1229,7 @@ describe('Component host element validation', () => {
})
class MySvgSink {}
const svgScriptHost = document.createElementNS('http://www.w3.org/2000/svg', 'script');
const svgScriptHost = document.createElementNS(SVG_NAMESPACE_URI, 'script');
document.head.appendChild(svgScriptHost);
try {
@@ -342,6 +342,7 @@
"areAnimationSupported",
"arrRemove",
"assertNotDestroyed",
"assertNotScriptHostElement",
"assertTypeDefined",
"attachPatchData",
"balancePreviousStylesIntoKeyframes",
@@ -267,6 +267,7 @@
"areAnimationSupported",
"arrRemove",
"assertNotDestroyed",
"assertNotScriptHostElement",
"assertTypeDefined",
"attachPatchData",
"baseElement",
@@ -313,6 +313,7 @@
"areAnimationSupported",
"arrRemove",
"assertNotDestroyed",
"assertNotScriptHostElement",
"assertTypeDefined",
"attachPatchData",
"bind",
@@ -372,6 +372,7 @@
"assertAllValuesPresent",
"assertControlPresent",
"assertNotDestroyed",
"assertNotScriptHostElement",
"assertPlatform",
"assertTypeDefined",
"attachPatchData",
@@ -376,6 +376,7 @@
"assertAllValuesPresent",
"assertControlPresent",
"assertNotDestroyed",
"assertNotScriptHostElement",
"assertPlatform",
"assertTypeDefined",
"attachPatchData",
@@ -310,6 +310,7 @@
"areAnimationSupported",
"arrRemove",
"assertNotDestroyed",
"assertNotScriptHostElement",
"assertTypeDefined",
"attachPatchData",
"baseElement",
@@ -148,6 +148,7 @@
"Location",
"LocationStrategy",
"MATH_ML_NAMESPACE",
"MATH_ML_NAMESPACE2",
"MATRIX_PARAM_SEGMENT_RE",
"MAXIMUM_REFRESH_RERUNS",
"MAXIMUM_REFRESH_RERUNS",
@@ -158,6 +159,7 @@
"MOVED_VIEWS",
"ModuleWithComponentFactories",
"NAMESPACE_URIS",
"NAMESPACE_URIS2",
"NATIVE",
"NAVIGATION_CANCELING_ERROR",
"NAVIGATION_ERROR_HANDLER",
@@ -239,7 +241,6 @@
"REMOVE_STYLES_ON_COMPONENT_DESTROY_DEFAULT",
"RENDERER",
"REQUIRED_UNSET_VALUE",
"RESOURCE_MAP",
"ROUTER_CONFIGURATION",
"ROUTER_OUTLET_DATA",
"ROUTER_PRELOADER",
@@ -275,12 +276,14 @@
"SAFE_URL_PATTERN",
"SCHEDULE_IN_ROOT_ZONE",
"SCHEDULE_IN_ROOT_ZONE_DEFAULT",
"SECURITY_SCHEMA",
"SEGMENT_RE",
"SIGNAL",
"SIGNAL_NODE",
"SIMPLE_CHANGES_STORE",
"STABILITY_WARNING_THRESHOLD",
"SVG_NAMESPACE",
"SVG_NAMESPACE2",
"SafeSubscriber",
"SafeValueImpl",
"Sanitizer",
@@ -354,6 +357,7 @@
"\\u0275\\u0275textInterpolate1",
"_DOM",
"_NullComponentFactoryResolver",
"_SECURITY_SCHEMA",
"_THROW_IF_NOT_FOUND",
"__asyncGenerator",
"__asyncValues",
@@ -424,6 +428,7 @@
"arrRemove",
"arrayEquals",
"assertNotDestroyed",
"assertNotScriptHostElement",
"assertTypeDefined",
"attachPatchData",
"baseElement",
@@ -446,6 +451,7 @@
"captureError",
"catchError",
"checkGuards",
"checkSecurityContext",
"checkStable",
"classIndexOf",
"cleanUpView",
@@ -723,6 +729,7 @@
"getRuntimeErrorCode",
"getSanitizationBypassType",
"getSanitizer",
"getSecurityContext",
"getSelectedIndex",
"getSelectedTNode",
"getSimpleChangesStore",
@@ -1000,6 +1007,7 @@
"redirectingNavigationError",
"refreshContentQueries",
"refreshView",
"registerContext",
"registerHostBindingOpCodes",
"registerLView",
"registerPostOrderHooks",
@@ -1022,6 +1030,7 @@
"resetPreOrderHookFlags",
"resolveData",
"resolveDirectives",
"resolveElement",
"resolveForwardRef",
"resolveNode",
"retrieveHydrationInfo",
@@ -1107,6 +1116,7 @@
"sortActivatedRouteSnapshots",
"sortByMatchingOutlets",
"split",
"splitNsName",
"splitQueryMultiSelectors",
"squashSegmentGroup",
"standardizeConfig",
@@ -248,6 +248,7 @@
"areAnimationSupported",
"arrRemove",
"assertNotDestroyed",
"assertNotScriptHostElement",
"assertTypeDefined",
"attachPatchData",
"baseElement",
@@ -85,6 +85,16 @@ describe('locale data api', () => {
expect(findLocaleData('de-CH')).toEqual(localeDeCH);
});
it('should not cache missing global locale data lookups', () => {
const localeEnNZ: any[] = ['en-NZ'];
expect(findLocaleData('en-NZ')).toEqual(localeEn);
global.ng.common.locales['en-nz'] = localeEnNZ;
expect(findLocaleData('en-NZ')).toBe(localeEnNZ);
});
it('should find the parent LOCALE_DATA if the exact locale is not available and the parent locale is on the global object', () => {
expect(findLocaleData('de-BE')).toEqual(localeDe);
});
@@ -368,6 +368,22 @@ describe('security integration tests', function () {
expect(link.getAttribute('href')).toEqual('unsafe:javascript:alert(1)');
});
it('should throw error on translated event attributes', () => {
const template = `<img src="/missing-image.png" onerror="void 0" i18n-onerror>`;
TestBed.overrideComponent(SecuredComponent, {set: {template}});
expect(() => TestBed.createComponent(SecuredComponent)).toThrowError(
/Translating attribute 'onerror' is disallowed for security reasons./,
);
});
it('should not throw error on translating "on" attribute', () => {
const template = `<div on="some-value" i18n-on></div>`;
TestBed.overrideComponent(SecuredComponent, {set: {template}});
expect(() => TestBed.createComponent(SecuredComponent)).not.toThrow();
});
it('should throw error on security-sensitive attributes with constant values', () => {
const template = `<iframe srcdoc="foo" i18n-srcdoc></iframe>`;
TestBed.overrideComponent(SecuredComponent, {set: {template}});
@@ -61,6 +61,13 @@ describe('sanitization', () => {
).toEqual('<img src="javascript:true">');
});
it('should not sanitize html sinks on concrete hosts with no HTML context', () => {
const html = '<script>evil</script><p>safe</p>';
expect(ɵɵsanitizeHtml(html, 'div', 'srcdoc')).toBe(html);
expect(ɵɵsanitizeHtml(html, 'iframe', 'srcdoc').toString()).toBe('<p>safe</p>');
});
it('should sanitize url', () => {
expect(ɵɵsanitizeUrl('http://server')).toEqual('http://server');
expect(ɵɵsanitizeUrl(new Wrap('http://server'))).toEqual('http://server');
@@ -111,7 +118,6 @@ describe('sanitization', () => {
// making sure security schema we have on compiler side is in sync with the `getUrlSanitizer`
// runtime function definition
const schema = SECURITY_SCHEMA();
const contextsByProp: Map<string, Set<number>> = new Map();
const sanitizerNameByContext: Map<number, Function> = new Map([
[SecurityContext.URL, ɵɵsanitizeUrl],
[SecurityContext.RESOURCE_URL, ɵɵsanitizeResourceUrl],
@@ -119,15 +125,9 @@ describe('sanitization', () => {
Object.entries(schema).forEach(([key, context]) => {
if (context === SecurityContext.URL || context === SecurityContext.RESOURCE_URL) {
const [tag, prop] = key.split('|');
const contexts = contextsByProp.get(prop) || new Set<number>();
contexts.add(context);
contextsByProp.set(prop, contexts);
// check only in case a prop can be a part of both URL contexts
if (contexts.size === 2) {
expect(getUrlSanitizer(tag, prop))
.withContext(`key: ${key}, context: ${context}`)
.toEqual(sanitizerNameByContext.get(context)!);
}
expect(getUrlSanitizer(tag, prop))
.withContext(`key: ${key}, context: ${context}`)
.toEqual(sanitizerNameByContext.get(context)!);
}
});
});
@@ -136,7 +136,8 @@ describe('sanitization', () => {
expect(getUrlSanitizer('IFRAME', 'SRC')).toEqual(ɵɵsanitizeResourceUrl);
expect(getUrlSanitizer('IFRAME', 'src')).toEqual(ɵɵsanitizeResourceUrl);
expect(getUrlSanitizer('iframe', 'SRC')).toEqual(ɵɵsanitizeResourceUrl);
expect(getUrlSanitizer('ScRiPt', 'xLiNk:HrEf')).toEqual(ɵɵsanitizeUrl);
expect(getUrlSanitizer('DiV', 'DaTa')).toBeNull();
expect(getUrlSanitizer('A', 'HREF')).toEqual(ɵɵsanitizeUrl);
});
@@ -149,10 +150,6 @@ describe('sanitization', () => {
expect(() => ɵɵsanitizeUrlOrResourceUrl('http://server', 'iframe', 'SRC')).toThrowError(ERROR);
expect(ɵɵsanitizeUrlOrResourceUrl('javascript:true', 'ScRiPt', 'xLiNk:HrEf')).toEqual(
'unsafe:javascript:true',
);
expect(ɵɵsanitizeUrlOrResourceUrl('javascript:true', 'A', 'HREF')).toEqual(
'unsafe:javascript:true',
);
@@ -193,6 +190,8 @@ describe('sanitization', () => {
expect(
ɵɵsanitizeUrlOrResourceUrl(bypassSanitizationTrustUrl('javascript:true'), 'a', 'href'),
).toEqual('javascript:true');
expect(ɵɵsanitizeUrlOrResourceUrl('javascript:true', 'div', 'data')).toBe('javascript:true');
});
it('should only trust constant strings from template literal tags without interpolation', () => {
+1 -1
View File
@@ -33,7 +33,7 @@
"./fesm2022/init.mjs"
],
"dependencies": {
"@babel/core": "7.29.0",
"@babel/core": "7.29.7",
"@types/babel__core": "7.20.5",
"tinyglobby": "^0.2.12",
"yargs": "^18.0.0"
+21 -3
View File
@@ -7,11 +7,14 @@
*/
import {DOCUMENT, ɵDomAdapter as DomAdapter, ɵgetDOM as getDOM} from '@angular/common';
import {Inject, Injectable} from '@angular/core';
import {Inject, Injectable, ɵRuntimeError as RuntimeError} from '@angular/core';
import {RuntimeErrorCode} from '../errors';
/**
* Represents the attributes of an HTML `<meta>` element. The element itself is
* represented by the internal `HTMLMetaElement`.
* represented by the internal `HTMLMetaElement`. Event handler attributes, which start with
* `on`, are not allowed.
*
* @see [HTML meta tag](https://developer.mozilla.org/docs/Web/HTML/Element/meta)
* @see {@link Meta}
@@ -133,6 +136,7 @@ export class Meta {
*/
updateTag(tag: MetaDefinition, selector?: string): HTMLMetaElement | null {
if (!tag) return null;
this._validateMetaDefinition(tag);
selector = selector || this._parseSelector(tag);
const meta: HTMLMetaElement = this.getTag(selector)!;
if (meta) {
@@ -164,6 +168,7 @@ export class Meta {
meta: MetaDefinition,
forceCreation: boolean = false,
): HTMLMetaElement {
this._validateMetaDefinition(meta);
if (!forceCreation) {
const selector: string = this._parseSelector(meta);
// It's allowed to have multiple elements with the same name so it's not enough to
@@ -197,6 +202,19 @@ export class Meta {
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}
private _validateMetaDefinition(tag: MetaDefinition): void {
for (const prop of Object.keys(tag)) {
const attributeName = this._getMetaKeyMap(prop);
if (attributeName.toLowerCase().startsWith('on')) {
throw new RuntimeError(
RuntimeErrorCode.INVALID_EVENT_ATTRIBUTE,
(typeof ngDevMode === 'undefined' || ngDevMode) &&
`The Meta service does not allow setting event handler attribute '${attributeName}' for security reasons.`,
);
}
}
}
private _containsAttributes(tag: MetaDefinition, elem: HTMLMetaElement): boolean {
return Object.keys(tag).every(
(key: string) => elem.getAttribute(this._getMetaKeyMap(key)) === tag[key],
@@ -204,7 +222,7 @@ export class Meta {
}
private _getMetaKeyMap(prop: string): string {
return META_KEYS_MAP[prop] || prop;
return Object.hasOwn(META_KEYS_MAP, prop) ? META_KEYS_MAP[prop] : prop;
}
}
+1
View File
@@ -26,6 +26,7 @@ export const enum RuntimeErrorCode {
SANITIZATION_UNSAFE_SCRIPT = 5200,
SANITIZATION_UNSAFE_RESOURCE_URL = 5201,
SANITIZATION_UNEXPECTED_CTX = 5202,
INVALID_EVENT_ATTRIBUTE = 5203,
// Animations related errors (5300-5400 range)
ANIMATION_RENDERER_ASYNC_LOADING_FAILURE = 5300,
@@ -89,6 +89,26 @@ describe('Meta service', () => {
expect(actual!.getAttribute('content')).toEqual('4321');
});
it('should reject event handler attributes targeting a body meta tag', () => {
doc.body.appendChild(defaultMeta);
const evil = 'alert(1)';
expect(metaService.getTag('property="fb:app_id"')).toBe(defaultMeta);
expect(() =>
metaService.updateTag({
property: 'fb:app_id',
style: 'content-visibility:auto',
oncontentvisibilityautostatechange: evil,
}),
).toThrowError(
/NG05203: The Meta service does not allow setting event handler attribute 'oncontentvisibilityautostatechange'/,
);
expect(defaultMeta.getAttribute('style')).toBeNull();
expect(defaultMeta.getAttribute('oncontentvisibilityautostatechange')).toBeNull();
});
it('should not allow a custom selector to match off target elements like the body tag', () => {
// This payload attempts to break out of the `meta[name="..."]` constraint entirely
// and inject a comma to target arbitrary DOM elements like the `body` tag via the
@@ -143,6 +163,42 @@ describe('Meta service', () => {
metaService.removeTagElement(actual);
});
it('should reject event handler attributes without adding a tag', () => {
const selector = 'name="og:title"';
const evil = 'alert(1)';
expect(() =>
metaService.addTag({
name: 'og:title',
style: 'content-visibility:auto',
oncontentvisibilityautostatechange: evil,
}),
).toThrowError(
/NG05203: The Meta service does not allow setting event handler attribute 'oncontentvisibilityautostatechange'/,
);
expect(metaService.getTag(selector)).toBeNull();
});
it('should reject event handler attributes in addTags without adding tags', () => {
const selector = 'name="og:title"';
const evil = 'alert(1)';
expect(() =>
metaService.addTags([
{
name: 'og:title',
style: 'content-visibility:auto',
oncontentvisibilityautostatechange: evil,
},
]),
).toThrowError(
/NG05203: The Meta service does not allow setting event handler attribute 'oncontentvisibilityautostatechange'/,
);
expect(metaService.getTag(selector)).toBeNull();
});
it('should add httpEquiv meta tag as http-equiv', () => {
metaService.addTag({httpEquiv: 'refresh', content: '3;url=http://test'});
@@ -155,6 +211,22 @@ describe('Meta service', () => {
metaService.removeTagElement(actual);
});
it('should add attributes whose names match Object prototype keys', () => {
const meta = metaService.addTag({
name: 'prototype-keys',
constructor: 'constructor',
toString: 'toString',
['__proto__']: '__proto__',
})!;
expect(meta.getAttribute('constructor')).toEqual('constructor');
expect(meta.getAttribute('toString')).toEqual('toString');
expect(meta.getAttribute('__proto__')).toEqual('__proto__');
// clean up
metaService.removeTagElement(meta);
});
it('should escape selector values when deriving the match selector', () => {
// This payload attempts to prematurely close the attribute selector
// and match another attribute.
-2
View File
@@ -57,8 +57,6 @@ export function resolveUrl(
return originUrl || null;
}
urlStr = urlStr.trim();
// Fast-path: if the URL is a valid, standard absolute URL, parse and return it immediately.
let resolved: URL | undefined;
try {
@@ -1583,6 +1583,17 @@ class HiddenModule {}
});
});
it('should resolve non-breaking space prefixed URLs as relative paths on the same origin', async () => {
ref.injector.get(NgZone).run(() => {
http.get('\u00A0//attacker.example/collect').subscribe((body) => {
expect(body).toEqual('success!');
});
mock
.expectOne('http://localhost:4000/%C2%A0//attacker.example/collect')
.flush('success!');
});
});
it('should reject backslash bypass SSRF attempts in relative requests and throw a suspicious origin error', async () => {
const badUrls = [
'/\\attacker.com',
@@ -1597,7 +1608,7 @@ class HiddenModule {}
next: () => fail(`Expected request for ${badUrl} to fail, but it succeeded.`),
error: (err) => {
expect(err.message).toBe(
`NG05703: URL ${badUrl.trim()} changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`,
`NG05703: URL ${badUrl} changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`,
);
},
});
@@ -1620,7 +1631,7 @@ class HiddenModule {}
next: () => fail(`Expected request for ${badUrl} to fail, but it succeeded.`),
error: (err) => {
expect(err.message).toBe(
`NG05703: URL ${badUrl.trim()} changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`,
`NG05703: URL ${badUrl} changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`,
);
},
});
+17
View File
@@ -74,6 +74,22 @@ describe('resolveUrl', () => {
`NG05703: URL ${url} changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`,
);
});
it('should not trim unicode whitespace into protocol-relative URLs', () => {
const urls = ['\u00A0//attacker.example/collect', '\uFEFF//attacker.example/collect'];
for (const urlStr of urls) {
const urlWithProtocolRelative = resolveUrl(urlStr, 'http://test.com', {
allowProtocolRelative: true,
});
expect(urlWithProtocolRelative.origin).toBe('http://test.com');
expect(urlWithProtocolRelative.pathname).toContain('//attacker.example/collect');
const urlWithoutProtocolRelative = resolveUrl(urlStr, 'http://test.com');
expect(urlWithoutProtocolRelative.origin).toBe('http://test.com');
expect(urlWithoutProtocolRelative.pathname).toContain('//attacker.example/collect');
}
});
});
describe('without origin', () => {
@@ -82,6 +98,7 @@ describe('resolveUrl', () => {
expect(resolveUrl('deep/path')).toBeNull();
expect(resolveUrl('/\\attacker.com/deep/path')).toBeNull();
expect(resolveUrl('\\\\attacker.com/deep/path')).toBeNull();
expect(resolveUrl('\u00A0//attacker.com/deep/path')).toBeNull();
});
it('should parse valid absolute URLs', () => {
+6 -4
View File
@@ -501,10 +501,10 @@ export abstract class AssetGroup {
* Create a new `Request` based on the specified URL and `RequestInit` options, preserving only
* metadata that are known to be safe.
*
* Currently, headers, redirect policy, an explicit `credentials: 'omit'`, and the HTTP cache
* mode are preserved. On cross-origin redirects, sensitive headers are removed. This includes
* `Authorization`, as required by the Fetch redirect algorithm, and forbidden request headers
* that could contain credentials.
* Currently, headers, referrer, referrer policy, redirect policy, an explicit
* `credentials: 'omit'`, and the HTTP cache mode are preserved. On cross-origin redirects,
* sensitive headers are removed. This includes `Authorization`, as required by the Fetch redirect
* algorithm, and forbidden request headers that could contain credentials.
*
* NOTE:
* `credentials: 'same-origin'` and `credentials: 'include'` are intentionally not preserved.
@@ -532,6 +532,8 @@ export abstract class AssetGroup {
const init: RequestInit = {
headers,
referrer: options.referrer,
referrerPolicy: options.referrerPolicy,
redirect: options.redirect,
};
@@ -1672,6 +1672,30 @@ import {envIsSupported} from '../testing/utils';
expect(bazReq.credentials).toBe('omit');
});
it(`passes 'referrer' through to the server`, async () => {
const reqInit = {referrer: 'http://localhost/profile?token=secret'};
expect(await makeRequest(scope, '/baz.txt', undefined, reqInit)).toBe('this is baz');
const [bazReq] = server.getRequestsFor('/baz.txt');
expect(bazReq.referrer).toBe('http://localhost/profile?token=secret');
});
it(`passes 'referrer: ""' through to the server`, async () => {
const reqInit = {referrer: ''};
expect(await makeRequest(scope, '/baz.txt', undefined, reqInit)).toBe('this is baz');
const [bazReq] = server.getRequestsFor('/baz.txt');
expect(bazReq.referrer).toBe('');
});
it(`passes 'referrerPolicy' through to the server`, async () => {
const reqInit = {referrerPolicy: 'no-referrer'};
expect(await makeRequest(scope, '/baz.txt', undefined, reqInit)).toBe('this is baz');
const [bazReq] = server.getRequestsFor('/baz.txt');
expect(bazReq.referrerPolicy).toBe('no-referrer');
});
it(`passes 'cache' through to the server`, async () => {
// Request a lazy-cached asset (so that it is fetched from the network) and provide an
// explicit HTTP cache mode.
@@ -1765,6 +1789,36 @@ import {envIsSupported} from '../testing/utils';
expect(redirectReq.credentials).toBe('omit');
});
it(`passes 'referrer' through to the server`, async () => {
const reqInit = {referrer: 'http://localhost/profile?token=secret'};
expect(await makeRequest(scope, '/lazy/redirected.txt', undefined, reqInit)).toBe(
'this was a redirect too',
);
const [redirectReq] = server.getRequestsFor('/lazy/redirect-target.txt');
expect(redirectReq.referrer).toBe('http://localhost/profile?token=secret');
});
it(`passes 'referrer: ""' through to the server`, async () => {
const reqInit = {referrer: ''};
expect(await makeRequest(scope, '/lazy/redirected.txt', undefined, reqInit)).toBe(
'this was a redirect too',
);
const [redirectReq] = server.getRequestsFor('/lazy/redirect-target.txt');
expect(redirectReq.referrer).toBe('');
});
it(`passes 'referrerPolicy' through to the server`, async () => {
const reqInit = {referrerPolicy: 'no-referrer'};
expect(await makeRequest(scope, '/lazy/redirected.txt', undefined, reqInit)).toBe(
'this was a redirect too',
);
const [redirectReq] = server.getRequestsFor('/lazy/redirect-target.txt');
expect(redirectReq.referrerPolicy).toBe('no-referrer');
});
it(`passes 'cache' through to the server`, async () => {
// Request a redirected, lazy-cached asset (so that it is fetched from the network) and
// provide an explicit HTTP cache mode.
@@ -130,8 +130,8 @@ export class MockRequest extends MockBody implements Request {
readonly method: string = 'GET';
readonly mode: RequestMode = 'cors';
readonly redirect: RequestRedirect = 'follow';
readonly referrer: string = '';
readonly referrerPolicy: ReferrerPolicy = 'no-referrer';
readonly referrer: string = 'about:client';
readonly referrerPolicy: ReferrerPolicy = '';
readonly signal: AbortSignal = null as any;
url: string;
@@ -163,6 +163,12 @@ export class MockRequest extends MockBody implements Request {
if (init.redirect !== undefined) {
this.redirect = init.redirect;
}
if (init.referrer !== undefined) {
this.referrer = init.referrer;
}
if (init.referrerPolicy !== undefined) {
this.referrerPolicy = init.referrerPolicy;
}
if (init.destination !== undefined) {
this.destination = init.destination;
}
@@ -179,6 +185,8 @@ export class MockRequest extends MockBody implements Request {
credentials: this.credentials,
headers: this.headers,
redirect: this.redirect,
referrer: this.referrer,
referrerPolicy: this.referrerPolicy,
});
}
}
+1 -1
View File
@@ -15,7 +15,7 @@
"@types/node": "^24.3.0",
"@types/systemjs": "6.15.4",
"bluebird": "3.7.2",
"domino": "https://github.com/angular/domino.git#f74cccd496283b6141fbdbeb25b168f4b1c9836a",
"domino": "https://github.com/angular/domino.git#04f987dc08ff3736b427f50941adf1722458528f",
"esbuild-plugin-umd-wrapper": "3.0.0",
"jest-environment-jsdom": "30.2.0",
"jest-environment-node": "30.2.0",
+1 -1
View File
@@ -9,7 +9,7 @@
"author": "",
"license": "MIT",
"dependencies": {
"domino": "https://github.com/angular/domino.git#f74cccd496283b6141fbdbeb25b168f4b1c9836a",
"domino": "https://github.com/angular/domino.git#04f987dc08ff3736b427f50941adf1722458528f",
"zone.js": "workspace:*"
},
"devDependencies": {
+363 -109
View File
@@ -89,14 +89,14 @@ importers:
specifier: 'workspace: *'
version: link:packages/upgrade
'@babel/cli':
specifier: 7.28.6
version: 7.28.6(@babel/core@7.29.0)
specifier: 7.29.7
version: 7.29.7(@babel/core@7.29.7)
'@babel/core':
specifier: 7.29.0
version: 7.29.0
specifier: 7.29.7
version: 7.29.7
'@babel/generator':
specifier: 7.29.1
version: 7.29.1
specifier: 7.29.7
version: 7.29.7
'@jridgewell/sourcemap-codec':
specifier: ^1.4.14
version: 1.5.5
@@ -105,7 +105,7 @@ importers:
version: 7.58.5(@types/node@20.19.39)
'@rollup/plugin-babel':
specifier: ^6.0.0
version: 6.1.0(@babel/core@7.29.0)(@types/babel__core@7.20.5)(rollup@4.57.1)
version: 6.1.0(@babel/core@7.29.7)(@types/babel__core@7.20.5)(rollup@4.57.1)
'@rollup/plugin-commonjs':
specifier: ^29.0.0
version: 29.0.2(rollup@4.57.1)
@@ -209,8 +209,8 @@ importers:
specifier: ^8.0.0
version: 8.0.3
domino:
specifier: https://github.com/angular/domino.git#f74cccd496283b6141fbdbeb25b168f4b1c9836a
version: '@angular/domino@https://codeload.github.com/angular/domino/tar.gz/f74cccd496283b6141fbdbeb25b168f4b1c9836a'
specifier: https://github.com/angular/domino.git#04f987dc08ff3736b427f50941adf1722458528f
version: '@angular/domino@https://codeload.github.com/angular/domino/tar.gz/04f987dc08ff3736b427f50941adf1722458528f'
esbuild:
specifier: 0.27.2
version: 0.27.2
@@ -340,10 +340,10 @@ importers:
version: https://codeload.github.com/angular/dev-infra-private-ng-dev-builds/tar.gz/4de8a14a1682d0f07e0b14a3b26498757c195904(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))
'@babel/plugin-proposal-async-generator-functions':
specifier: 7.20.7
version: 7.20.7(@babel/core@7.29.0)
version: 7.20.7(@babel/core@7.29.7)
'@babel/plugin-transform-async-generator-functions':
specifier: ^7.27.1
version: 7.29.0(@babel/core@7.29.0)
specifier: ^7.29.7
version: 7.29.7(@babel/core@7.29.7)
'@bazel/bazelisk':
specifier: ^1.7.5
version: 1.28.1
@@ -1034,8 +1034,8 @@ importers:
specifier: 'workspace: *'
version: link:../compiler
'@babel/core':
specifier: 7.29.0
version: 7.29.0
specifier: 7.29.7
version: 7.29.7
'@jridgewell/sourcemap-codec':
specifier: ^1.4.14
version: 1.5.5
@@ -1159,8 +1159,8 @@ importers:
specifier: 'workspace: *'
version: link:../compiler-cli
'@babel/core':
specifier: 7.29.0
version: 7.29.0
specifier: 7.29.7
version: 7.29.7
'@types/babel__core':
specifier: 7.20.5
version: 7.20.5
@@ -1304,8 +1304,8 @@ importers:
specifier: 3.7.2
version: 3.7.2
domino:
specifier: https://github.com/angular/domino.git#f74cccd496283b6141fbdbeb25b168f4b1c9836a
version: '@angular/domino@https://codeload.github.com/angular/domino/tar.gz/f74cccd496283b6141fbdbeb25b168f4b1c9836a'
specifier: https://github.com/angular/domino.git#04f987dc08ff3736b427f50941adf1722458528f
version: '@angular/domino@https://codeload.github.com/angular/domino/tar.gz/04f987dc08ff3736b427f50941adf1722458528f'
esbuild-plugin-umd-wrapper:
specifier: 3.0.0
version: 3.0.0
@@ -1352,8 +1352,8 @@ importers:
packages/zone.js/test/typings:
dependencies:
domino:
specifier: https://github.com/angular/domino.git#f74cccd496283b6141fbdbeb25b168f4b1c9836a
version: '@angular/domino@https://codeload.github.com/angular/domino/tar.gz/f74cccd496283b6141fbdbeb25b168f4b1c9836a'
specifier: https://github.com/angular/domino.git#04f987dc08ff3736b427f50941adf1722458528f
version: '@angular/domino@https://codeload.github.com/angular/domino/tar.gz/04f987dc08ff3736b427f50941adf1722458528f'
zone.js:
specifier: workspace:*
version: link:../..
@@ -1780,10 +1780,9 @@ packages:
'@angular/compiler':
optional: true
'@angular/domino@https://codeload.github.com/angular/domino/tar.gz/f74cccd496283b6141fbdbeb25b168f4b1c9836a':
resolution: {tarball: https://codeload.github.com/angular/domino/tar.gz/f74cccd496283b6141fbdbeb25b168f4b1c9836a}
'@angular/domino@https://codeload.github.com/angular/domino/tar.gz/04f987dc08ff3736b427f50941adf1722458528f':
resolution: {tarball: https://codeload.github.com/angular/domino/tar.gz/04f987dc08ff3736b427f50941adf1722458528f}
version: 2.1.6
engines: {npm: Please use pnpm instead of NPM to install dependencies, pnpm: 10.31.0, yarn: Please use pnpm instead of Yarn to install dependencies}
'@angular/material@21.2.9':
resolution: {integrity: sha512-uU5Sy0rSd4Y4WjqTcrqs3MpfY/Uy5tmDPSAAvwD0y5y4QVOLoV8uhTQDI/nNg2Lh9NoJKvykZE1ITRMvjfRALQ==}
@@ -1889,8 +1888,8 @@ packages:
resolution: {integrity: sha512-LqT8mRZpEils9zGR9eW+Ljqifh2aMA99UF/X0jxIKDYZeHr6onlHwhVP4xHCeLhh55BI63JCbdf1iWJbMh1mPw==}
engines: {node: '>=20'}
'@babel/cli@7.28.6':
resolution: {integrity: sha512-6EUNcuBbNkj08Oj4gAZ+BUU8yLCgKzgVX4gaTh09Ya2C8ICM4P+G30g4m3akRxSYAp3A/gnWchrNst7px4/nUQ==}
'@babel/cli@7.29.7':
resolution: {integrity: sha512-/75HwRbAYPqXv/Ax1h7Fg3IZfXgdU98jnA8H93/m/QBaPV3Hp5ICoLqzGYye1yHBCgpmXvtqgSUN8oOKX5tojQ==}
engines: {node: '>=6.9.0'}
hasBin: true
peerDependencies:
@@ -1900,10 +1899,18 @@ packages:
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
engines: {node: '>=6.9.0'}
'@babel/code-frame@7.29.7':
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
engines: {node: '>=6.9.0'}
'@babel/compat-data@7.29.0':
resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==}
engines: {node: '>=6.9.0'}
'@babel/compat-data@7.29.7':
resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==}
engines: {node: '>=6.9.0'}
'@babel/core@7.26.10':
resolution: {integrity: sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==}
engines: {node: '>=6.9.0'}
@@ -1912,18 +1919,34 @@ packages:
resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==}
engines: {node: '>=6.9.0'}
'@babel/core@7.29.7':
resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==}
engines: {node: '>=6.9.0'}
'@babel/generator@7.29.1':
resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==}
engines: {node: '>=6.9.0'}
'@babel/generator@7.29.7':
resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==}
engines: {node: '>=6.9.0'}
'@babel/helper-annotate-as-pure@7.27.3':
resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==}
engines: {node: '>=6.9.0'}
'@babel/helper-annotate-as-pure@7.29.7':
resolution: {integrity: sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==}
engines: {node: '>=6.9.0'}
'@babel/helper-compilation-targets@7.28.6':
resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==}
engines: {node: '>=6.9.0'}
'@babel/helper-compilation-targets@7.29.7':
resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==}
engines: {node: '>=6.9.0'}
'@babel/helper-create-class-features-plugin@7.28.6':
resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==}
engines: {node: '>=6.9.0'}
@@ -1949,6 +1972,10 @@ packages:
resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
engines: {node: '>=6.9.0'}
'@babel/helper-globals@7.29.7':
resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==}
engines: {node: '>=6.9.0'}
'@babel/helper-member-expression-to-functions@7.28.5':
resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==}
engines: {node: '>=6.9.0'}
@@ -1957,12 +1984,22 @@ packages:
resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==}
engines: {node: '>=6.9.0'}
'@babel/helper-module-imports@7.29.7':
resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==}
engines: {node: '>=6.9.0'}
'@babel/helper-module-transforms@7.28.6':
resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
'@babel/helper-module-transforms@7.29.7':
resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
'@babel/helper-optimise-call-expression@7.27.1':
resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==}
engines: {node: '>=6.9.0'}
@@ -1971,12 +2008,22 @@ packages:
resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==}
engines: {node: '>=6.9.0'}
'@babel/helper-plugin-utils@7.29.7':
resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==}
engines: {node: '>=6.9.0'}
'@babel/helper-remap-async-to-generator@7.27.1':
resolution: {integrity: sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
'@babel/helper-remap-async-to-generator@7.29.7':
resolution: {integrity: sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
'@babel/helper-replace-supers@7.28.6':
resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==}
engines: {node: '>=6.9.0'}
@@ -1995,27 +2042,52 @@ packages:
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
'@babel/helper-string-parser@7.29.7':
resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.28.5':
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.29.7':
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-option@7.27.1':
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-option@7.29.7':
resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==}
engines: {node: '>=6.9.0'}
'@babel/helper-wrap-function@7.28.6':
resolution: {integrity: sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==}
engines: {node: '>=6.9.0'}
'@babel/helper-wrap-function@7.29.7':
resolution: {integrity: sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==}
engines: {node: '>=6.9.0'}
'@babel/helpers@7.29.2':
resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==}
engines: {node: '>=6.9.0'}
'@babel/helpers@7.29.7':
resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==}
engines: {node: '>=6.9.0'}
'@babel/parser@7.29.2':
resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==}
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/parser@7.29.7':
resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==}
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5':
resolution: {integrity: sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==}
engines: {node: '>=6.9.0'}
@@ -2174,6 +2246,12 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/plugin-transform-async-generator-functions@7.29.7':
resolution: {integrity: sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/plugin-transform-async-to-generator@7.28.6':
resolution: {integrity: sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==}
engines: {node: '>=6.9.0'}
@@ -2493,14 +2571,26 @@ packages:
resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
engines: {node: '>=6.9.0'}
'@babel/template@7.29.7':
resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
engines: {node: '>=6.9.0'}
'@babel/traverse@7.29.0':
resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==}
engines: {node: '>=6.9.0'}
'@babel/traverse@7.29.7':
resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==}
engines: {node: '>=6.9.0'}
'@babel/types@7.29.0':
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
engines: {node: '>=6.9.0'}
'@babel/types@7.29.7':
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
engines: {node: '>=6.9.0'}
'@bazel/bazelisk@1.28.1':
resolution: {integrity: sha512-K21x83NXOtd0yb2qzjMES3UV4xEWZ1q1vnXFhADA1u7IoiMVQkJAVQRK3oZ5txpnrGafY15HS+YYr2nmsEP4Tg==}
hasBin: true
@@ -14266,7 +14356,7 @@ snapshots:
optionalDependencies:
'@angular/compiler': link:packages/compiler
'@angular/domino@https://codeload.github.com/angular/domino/tar.gz/f74cccd496283b6141fbdbeb25b168f4b1c9836a': {}
'@angular/domino@https://codeload.github.com/angular/domino/tar.gz/04f987dc08ff3736b427f50941adf1722458528f': {}
'@angular/material@21.2.9(@angular/cdk@21.2.9(@angular/common@packages+common)(@angular/core@packages+core)(@angular/platform-browser@packages+platform-browser)(rxjs@7.8.2))(@angular/common@packages+common)(@angular/core@packages+core)(@angular/forms@packages+forms)(@angular/platform-browser@packages+platform-browser)(rxjs@7.8.2)':
dependencies:
@@ -14492,9 +14582,9 @@ snapshots:
jsonwebtoken: 9.0.3
uuid: 8.3.2
'@babel/cli@7.28.6(@babel/core@7.29.0)':
'@babel/cli@7.29.7(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@jridgewell/trace-mapping': 0.3.31
commander: 6.2.1
convert-source-map: 2.0.0
@@ -14512,13 +14602,21 @@ snapshots:
js-tokens: 4.0.0
picocolors: 1.1.1
'@babel/code-frame@7.29.7':
dependencies:
'@babel/helper-validator-identifier': 7.29.7
js-tokens: 4.0.0
picocolors: 1.1.1
'@babel/compat-data@7.29.0': {}
'@babel/compat-data@7.29.7': {}
'@babel/core@7.26.10':
dependencies:
'@ampproject/remapping': 2.3.0
'@babel/code-frame': 7.29.0
'@babel/generator': 7.29.1
'@babel/generator': 7.29.7
'@babel/helper-compilation-targets': 7.28.6
'@babel/helper-module-transforms': 7.28.6(@babel/core@7.26.10)
'@babel/helpers': 7.29.2
@@ -14537,7 +14635,7 @@ snapshots:
'@babel/core@7.29.0':
dependencies:
'@babel/code-frame': 7.29.0
'@babel/generator': 7.29.1
'@babel/generator': 7.29.7
'@babel/helper-compilation-targets': 7.28.6
'@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
'@babel/helpers': 7.29.2
@@ -14554,6 +14652,26 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@babel/core@7.29.7':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/generator': 7.29.7
'@babel/helper-compilation-targets': 7.29.7
'@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7)
'@babel/helpers': 7.29.7
'@babel/parser': 7.29.7
'@babel/template': 7.29.7
'@babel/traverse': 7.29.7
'@babel/types': 7.29.7
'@jridgewell/remapping': 2.3.5
convert-source-map: 2.0.0
debug: 4.4.3(supports-color@10.2.2)
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
transitivePeerDependencies:
- supports-color
'@babel/generator@7.29.1':
dependencies:
'@babel/parser': 7.29.2
@@ -14562,10 +14680,22 @@ snapshots:
'@jridgewell/trace-mapping': 0.3.31
jsesc: 3.1.0
'@babel/generator@7.29.7':
dependencies:
'@babel/parser': 7.29.7
'@babel/types': 7.29.7
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
jsesc: 3.1.0
'@babel/helper-annotate-as-pure@7.27.3':
dependencies:
'@babel/types': 7.29.0
'@babel/helper-annotate-as-pure@7.29.7':
dependencies:
'@babel/types': 7.29.7
'@babel/helper-compilation-targets@7.28.6':
dependencies:
'@babel/compat-data': 7.29.0
@@ -14574,6 +14704,14 @@ snapshots:
lru-cache: 5.1.1
semver: 6.3.1
'@babel/helper-compilation-targets@7.29.7':
dependencies:
'@babel/compat-data': 7.29.7
'@babel/helper-validator-option': 7.29.7
browserslist: 4.28.2
lru-cache: 5.1.1
semver: 6.3.1
'@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
@@ -14611,6 +14749,8 @@ snapshots:
'@babel/helper-globals@7.28.0': {}
'@babel/helper-globals@7.29.7': {}
'@babel/helper-member-expression-to-functions@7.28.5':
dependencies:
'@babel/traverse': 7.29.0
@@ -14625,6 +14765,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@babel/helper-module-imports@7.29.7':
dependencies:
'@babel/traverse': 7.29.7
'@babel/types': 7.29.7
transitivePeerDependencies:
- supports-color
'@babel/helper-module-transforms@7.28.6(@babel/core@7.26.10)':
dependencies:
'@babel/core': 7.26.10
@@ -14643,12 +14790,23 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.29.7
'@babel/helper-module-imports': 7.29.7
'@babel/helper-validator-identifier': 7.29.7
'@babel/traverse': 7.29.7
transitivePeerDependencies:
- supports-color
'@babel/helper-optimise-call-expression@7.27.1':
dependencies:
'@babel/types': 7.29.0
'@babel/helper-plugin-utils@7.28.6': {}
'@babel/helper-plugin-utils@7.29.7': {}
'@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
@@ -14658,6 +14816,33 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.29.7
'@babel/helper-annotate-as-pure': 7.27.3
'@babel/helper-wrap-function': 7.28.6
'@babel/traverse': 7.29.0
transitivePeerDependencies:
- supports-color
'@babel/helper-remap-async-to-generator@7.29.7(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-annotate-as-pure': 7.29.7
'@babel/helper-wrap-function': 7.29.7
'@babel/traverse': 7.29.7
transitivePeerDependencies:
- supports-color
'@babel/helper-remap-async-to-generator@7.29.7(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.29.7
'@babel/helper-annotate-as-pure': 7.29.7
'@babel/helper-wrap-function': 7.29.7
'@babel/traverse': 7.29.7
transitivePeerDependencies:
- supports-color
'@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
@@ -14680,10 +14865,16 @@ snapshots:
'@babel/helper-string-parser@7.27.1': {}
'@babel/helper-string-parser@7.29.7': {}
'@babel/helper-validator-identifier@7.28.5': {}
'@babel/helper-validator-identifier@7.29.7': {}
'@babel/helper-validator-option@7.27.1': {}
'@babel/helper-validator-option@7.29.7': {}
'@babel/helper-wrap-function@7.28.6':
dependencies:
'@babel/template': 7.28.6
@@ -14692,15 +14883,32 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@babel/helper-wrap-function@7.29.7':
dependencies:
'@babel/template': 7.29.7
'@babel/traverse': 7.29.7
'@babel/types': 7.29.7
transitivePeerDependencies:
- supports-color
'@babel/helpers@7.29.2':
dependencies:
'@babel/template': 7.28.6
'@babel/types': 7.29.0
'@babel/helpers@7.29.7':
dependencies:
'@babel/template': 7.29.7
'@babel/types': 7.29.7
'@babel/parser@7.29.2':
dependencies:
'@babel/types': 7.29.0
'@babel/parser@7.29.7':
dependencies:
'@babel/types': 7.29.7
'@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
@@ -14736,13 +14944,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@babel/plugin-proposal-async-generator-functions@7.20.7(@babel/core@7.29.0)':
'@babel/plugin-proposal-async-generator-functions@7.20.7(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@babel/helper-environment-visitor': 7.24.7
'@babel/helper-plugin-utils': 7.28.6
'@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0)
'@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0)
'@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.7)
'@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.7)
transitivePeerDependencies:
- supports-color
@@ -14750,24 +14958,24 @@ snapshots:
dependencies:
'@babel/core': 7.29.0
'@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)':
'@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)':
'@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)':
'@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)':
'@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-import-assertions@7.28.6(@babel/core@7.29.0)':
@@ -14780,64 +14988,69 @@ snapshots:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)':
'@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)':
'@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)':
'@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)':
'@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)':
'@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)':
'@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)':
'@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)':
'@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)':
'@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)':
'@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)':
'@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)':
'@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.29.7
'@babel/helper-plugin-utils': 7.28.6
'@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.0)':
@@ -14860,6 +15073,24 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@babel/plugin-transform-async-generator-functions@7.29.7(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.29.7
'@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.0)
'@babel/traverse': 7.29.7
transitivePeerDependencies:
- supports-color
'@babel/plugin-transform-async-generator-functions@7.29.7(@babel/core@7.29.7)':
dependencies:
'@babel/core': 7.29.7
'@babel/helper-plugin-utils': 7.29.7
'@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.7)
'@babel/traverse': 7.29.7
transitivePeerDependencies:
- supports-color
'@babel/plugin-transform-async-to-generator@7.28.6(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
@@ -15208,7 +15439,7 @@ snapshots:
'@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0)
'@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.0)
'@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0)
'@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0)
'@babel/plugin-transform-async-generator-functions': 7.29.7(@babel/core@7.29.0)
'@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0)
'@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.29.0)
'@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0)
@@ -15282,10 +15513,16 @@ snapshots:
'@babel/parser': 7.29.2
'@babel/types': 7.29.0
'@babel/template@7.29.7':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/parser': 7.29.7
'@babel/types': 7.29.7
'@babel/traverse@7.29.0':
dependencies:
'@babel/code-frame': 7.29.0
'@babel/generator': 7.29.1
'@babel/generator': 7.29.7
'@babel/helper-globals': 7.28.0
'@babel/parser': 7.29.2
'@babel/template': 7.28.6
@@ -15294,11 +15531,28 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@babel/traverse@7.29.7':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/generator': 7.29.7
'@babel/helper-globals': 7.29.7
'@babel/parser': 7.29.7
'@babel/template': 7.29.7
'@babel/types': 7.29.7
debug: 4.4.3(supports-color@10.2.2)
transitivePeerDependencies:
- supports-color
'@babel/types@7.29.0':
dependencies:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
'@babel/types@7.29.7':
dependencies:
'@babel/helper-string-parser': 7.29.7
'@babel/helper-validator-identifier': 7.29.7
'@bazel/bazelisk@1.28.1': {}
'@bazel/buildifier@8.2.1': {}
@@ -17040,7 +17294,7 @@ snapshots:
'@jest/transform@30.2.0':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@jest/types': 30.2.0
'@jridgewell/trace-mapping': 0.3.31
babel-plugin-istanbul: 7.0.1
@@ -18106,9 +18360,9 @@ snapshots:
'@rolldown/pluginutils@1.0.0-rc.4': {}
'@rollup/plugin-babel@6.1.0(@babel/core@7.29.0)(@types/babel__core@7.20.5)(rollup@4.57.1)':
'@rollup/plugin-babel@6.1.0(@babel/core@7.29.7)(@types/babel__core@7.20.5)(rollup@4.57.1)':
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@babel/helper-module-imports': 7.28.6
'@rollup/pluginutils': 5.3.0(rollup@4.57.1)
optionalDependencies:
@@ -19716,13 +19970,13 @@ snapshots:
b4a@1.8.0: {}
babel-jest@30.2.0(@babel/core@7.29.0):
babel-jest@30.2.0(@babel/core@7.29.7):
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@jest/transform': 30.2.0
'@types/babel__core': 7.20.5
babel-plugin-istanbul: 7.0.1
babel-preset-jest: 30.2.0(@babel/core@7.29.0)
babel-preset-jest: 30.2.0(@babel/core@7.29.7)
chalk: 4.1.2
graceful-fs: 4.2.11
slash: 3.0.0
@@ -19781,30 +20035,30 @@ snapshots:
transitivePeerDependencies:
- supports-color
babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0):
babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.7):
dependencies:
'@babel/core': 7.29.0
'@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0)
'@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0)
'@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0)
'@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0)
'@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0)
'@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0)
'@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0)
'@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0)
'@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0)
'@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0)
'@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0)
'@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0)
'@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0)
'@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0)
'@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0)
'@babel/core': 7.29.7
'@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.7)
'@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.7)
'@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.7)
'@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.7)
'@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.7)
'@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.7)
'@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.7)
'@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.7)
'@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.7)
'@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.7)
'@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.7)
'@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.7)
'@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.7)
'@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.7)
'@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.7)
babel-preset-jest@30.2.0(@babel/core@7.29.0):
babel-preset-jest@30.2.0(@babel/core@7.29.7):
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
babel-plugin-jest-hoist: 30.2.0
babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0)
babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.7)
bach@2.0.1:
dependencies:
@@ -23248,7 +23502,7 @@ snapshots:
istanbul-lib-instrument@5.2.1:
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@babel/parser': 7.29.2
'@istanbuljs/schema': 0.1.6
istanbul-lib-coverage: 3.2.2
@@ -23258,7 +23512,7 @@ snapshots:
istanbul-lib-instrument@6.0.3:
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@babel/parser': 7.29.2
'@istanbuljs/schema': 0.1.6
istanbul-lib-coverage: 3.2.2
@@ -23415,12 +23669,12 @@ snapshots:
jest-config@30.2.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)):
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@jest/get-type': 30.1.0
'@jest/pattern': 30.0.1
'@jest/test-sequencer': 30.2.0
'@jest/types': 30.2.0
babel-jest: 30.2.0(@babel/core@7.29.0)
babel-jest: 30.2.0(@babel/core@7.29.7)
chalk: 4.1.2
ci-info: 4.4.0
deepmerge: 4.3.1
@@ -23449,12 +23703,12 @@ snapshots:
jest-config@30.2.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)):
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@jest/get-type': 30.1.0
'@jest/pattern': 30.0.1
'@jest/test-sequencer': 30.2.0
'@jest/types': 30.2.0
babel-jest: 30.2.0(@babel/core@7.29.0)
babel-jest: 30.2.0(@babel/core@7.29.7)
chalk: 4.1.2
ci-info: 4.4.0
deepmerge: 4.3.1
@@ -23483,12 +23737,12 @@ snapshots:
jest-config@30.2.0(@types/node@24.10.11)(ts-node@10.9.2(@types/node@24.12.2)(typescript@6.0.3)):
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@jest/get-type': 30.1.0
'@jest/pattern': 30.0.1
'@jest/test-sequencer': 30.2.0
'@jest/types': 30.2.0
babel-jest: 30.2.0(@babel/core@7.29.0)
babel-jest: 30.2.0(@babel/core@7.29.7)
chalk: 4.1.2
ci-info: 4.4.0
deepmerge: 4.3.1
@@ -23516,12 +23770,12 @@ snapshots:
jest-config@30.2.0(@types/node@24.12.2)(ts-node@10.9.2(@types/node@24.12.2)(typescript@6.0.3)):
dependencies:
'@babel/core': 7.29.0
'@babel/core': 7.29.7
'@jest/get-type': 30.1.0
'@jest/pattern': 30.0.1
'@jest/test-sequencer': 30.2.0
'@jest/types': 30.2.0
babel-jest: 30.2.0(@babel/core@7.29.0)
babel-jest: 30.2.0(@babel/core@7.29.7)
chalk: 4.1.2
ci-info: 4.4.0
deepmerge: 4.3.1
@@ -23713,17 +23967,17 @@ snapshots:
jest-snapshot@30.2.0:
dependencies:
'@babel/core': 7.29.0
'@babel/generator': 7.29.1
'@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0)
'@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0)
'@babel/core': 7.29.7
'@babel/generator': 7.29.7
'@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.7)
'@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.7)
'@babel/types': 7.29.0
'@jest/expect-utils': 30.2.0
'@jest/get-type': 30.1.0
'@jest/snapshot-utils': 30.2.0
'@jest/transform': 30.2.0
'@jest/types': 30.2.0
babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0)
babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.7)
chalk: 4.1.2
expect: 30.2.0
graceful-fs: 4.2.11