mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c0bc4345a | |||
| 5aa6d97deb | |||
| 73d8bbd27c | |||
| 992d627b69 | |||
| c19a36c2fb | |||
| dca0a77488 | |||
| caf616670f | |||
| fec5977df4 | |||
| e33d69a71c | |||
| 6afe6fa781 | |||
| 07cb840eac | |||
| 7b884f585a | |||
| 948a8d6831 | |||
| e2660c3dee | |||
| f34a93c946 | |||
| 9949dccce1 | |||
| 5d00aa2bd4 | |||
| e86c31bf26 | |||
| 1804f73bec | |||
| 91df739b80 | |||
| 8d22cc953b | |||
| 5a693bafcd | |||
| 6bcce117fb |
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]},
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'},
|
||||
|
||||
@@ -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",
|
||||
|
||||
+57
@@ -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
|
||||
|
||||
+22
-4
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+30
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.`,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Generated
+363
-109
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user