@angular/ssr 20.3.16 → 20.3.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/fesm2022/ssr.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { validateRequest, cloneRequestAndPatchHeaders } from './validation.mjs';
1
2
  import { ɵConsole as _Console, ApplicationRef, REQUEST, InjectionToken, provideEnvironmentInitializer, inject, makeEnvironmentProviders, ɵENABLE_ROOT_COMPONENT_BOOTSTRAP as _ENABLE_ROOT_COMPONENT_BOOTSTRAP, Compiler, createEnvironmentInjector, EnvironmentInjector, runInInjectionContext, ɵresetCompiledComponents as _resetCompiledComponents, REQUEST_CONTEXT, RESPONSE_INIT, LOCALE_ID } from '@angular/core';
2
3
  import { platformServer, INITIAL_CONFIG, ɵSERVER_CONTEXT as _SERVER_CONTEXT, ɵrenderInternal as _renderInternal, provideServerRendering as provideServerRendering$1 } from '@angular/platform-server';
3
4
  import { ActivatedRoute, Router, ROUTES, ɵloadChildren as _loadChildren } from '@angular/router';
@@ -216,24 +217,26 @@ function addTrailingSlash(url) {
216
217
  * ```
217
218
  */
218
219
  function joinUrlParts(...parts) {
219
- const normalizeParts = [];
220
+ const normalizedParts = [];
220
221
  for (const part of parts) {
221
222
  if (part === '') {
222
223
  // Skip any empty parts
223
224
  continue;
224
225
  }
225
- let normalizedPart = part;
226
- if (part[0] === '/') {
227
- normalizedPart = normalizedPart.slice(1);
226
+ let start = 0;
227
+ let end = part.length;
228
+ // Use "Pointers" to avoid intermediate slices
229
+ while (start < end && part[start] === '/') {
230
+ start++;
228
231
  }
229
- if (part[part.length - 1] === '/') {
230
- normalizedPart = normalizedPart.slice(0, -1);
232
+ while (end > start && part[end - 1] === '/') {
233
+ end--;
231
234
  }
232
- if (normalizedPart !== '') {
233
- normalizeParts.push(normalizedPart);
235
+ if (start < end) {
236
+ normalizedParts.push(part.slice(start, end));
234
237
  }
235
238
  }
236
- return addLeadingSlash(normalizeParts.join('/'));
239
+ return addLeadingSlash(normalizedParts.join('/'));
237
240
  }
238
241
  /**
239
242
  * Strips `/index.html` from the end of a URL's path, if present.
@@ -2242,6 +2245,21 @@ class AngularServerApp {
2242
2245
  }
2243
2246
  return html;
2244
2247
  }
2248
+ /**
2249
+ * Serves the client-side version of the application.
2250
+ * TODO(alanagius): Remove this method in version 22.
2251
+ * @internal
2252
+ */
2253
+ async serveClientSidePage() {
2254
+ const { manifest: { locale }, assets, } = this;
2255
+ const html = await assets.getServerAsset('index.csr.html').text();
2256
+ return new Response(html, {
2257
+ headers: new Headers({
2258
+ 'Content-Type': 'text/html;charset=UTF-8',
2259
+ ...(locale !== undefined ? { 'Content-Language': locale } : {}),
2260
+ }),
2261
+ });
2262
+ }
2245
2263
  }
2246
2264
  let angularServerApp;
2247
2265
  /**
@@ -2517,6 +2535,10 @@ class AngularAppEngine {
2517
2535
  * The manifest for the server application.
2518
2536
  */
2519
2537
  manifest = getAngularAppEngineManifest();
2538
+ /**
2539
+ * A set of allowed hostnames for the server application.
2540
+ */
2541
+ allowedHosts;
2520
2542
  /**
2521
2543
  * A map of supported locales from the server application's manifest.
2522
2544
  */
@@ -2525,6 +2547,13 @@ class AngularAppEngine {
2525
2547
  * A cache that holds entry points, keyed by their potential locale string.
2526
2548
  */
2527
2549
  entryPointsCache = new Map();
2550
+ /**
2551
+ * Creates a new instance of the Angular server application engine.
2552
+ * @param options Options for the Angular server application engine.
2553
+ */
2554
+ constructor(options) {
2555
+ this.allowedHosts = new Set([...(options?.allowedHosts ?? []), ...this.manifest.allowedHosts]);
2556
+ }
2528
2557
  /**
2529
2558
  * Handles an incoming HTTP request by serving prerendered content, performing server-side rendering,
2530
2559
  * or delivering a static file for client-side rendered routes based on the `RenderMode` setting.
@@ -2535,11 +2564,35 @@ class AngularAppEngine {
2535
2564
  *
2536
2565
  * @remarks A request to `https://www.example.com/page/index.html` will serve or render the Angular route
2537
2566
  * corresponding to `https://www.example.com/page`.
2567
+ *
2568
+ * @remarks
2569
+ * To prevent potential Server-Side Request Forgery (SSRF), this function verifies the hostname
2570
+ * of the `request.url` against a list of authorized hosts.
2571
+ * If the hostname is not recognized and `allowedHosts` is not empty, a Client-Side Rendered (CSR) version of the
2572
+ * page is returned otherwise a 400 Bad Request is returned.
2573
+ * Resolution:
2574
+ * Authorize your hostname by configuring `allowedHosts` in `angular.json` in:
2575
+ * `projects.[project-name].architect.build.options.security.allowedHosts`.
2576
+ * Alternatively, you pass it directly through the configuration options of `AngularAppEngine`.
2577
+ *
2578
+ * For more information see: https://angular.dev/best-practices/security#preventing-server-side-request-forgery-ssrf
2538
2579
  */
2539
2580
  async handle(request, requestContext) {
2540
- const serverApp = await this.getAngularServerAppForRequest(request);
2581
+ const allowedHost = this.allowedHosts;
2582
+ try {
2583
+ validateRequest(request, allowedHost);
2584
+ }
2585
+ catch (error) {
2586
+ return this.handleValidationError(error, request);
2587
+ }
2588
+ // Clone request with patched headers to prevent unallowed host header access.
2589
+ const { request: securedRequest, onError: onHeaderValidationError } = cloneRequestAndPatchHeaders(request, allowedHost);
2590
+ const serverApp = await this.getAngularServerAppForRequest(securedRequest);
2541
2591
  if (serverApp) {
2542
- return serverApp.handle(request, requestContext);
2592
+ return Promise.race([
2593
+ onHeaderValidationError.then((error) => this.handleValidationError(error, securedRequest)),
2594
+ serverApp.handle(securedRequest, requestContext),
2595
+ ]);
2543
2596
  }
2544
2597
  if (this.supportedLocales.length > 1) {
2545
2598
  // Redirect to the preferred language if i18n is enabled.
@@ -2645,6 +2698,37 @@ class AngularAppEngine {
2645
2698
  const potentialLocale = getPotentialLocaleIdFromUrl(url, basePath);
2646
2699
  return this.getEntryPointExports(potentialLocale) ?? this.getEntryPointExports('');
2647
2700
  }
2701
+ /**
2702
+ * Handles validation errors by logging the error and returning an appropriate response.
2703
+ *
2704
+ * @param error - The validation error to handle.
2705
+ * @param request - The HTTP request that caused the validation error.
2706
+ * @returns A promise that resolves to a `Response` object with a 400 status code if allowed hosts are configured,
2707
+ * or `null` if allowed hosts are not configured (in which case the request is served client-side).
2708
+ */
2709
+ async handleValidationError(error, request) {
2710
+ const isAllowedHostConfigured = this.allowedHosts.size > 0;
2711
+ const errorMessage = error.message;
2712
+ // eslint-disable-next-line no-console
2713
+ console.error(`ERROR: Bad Request ("${request.url}").\n` +
2714
+ errorMessage +
2715
+ (isAllowedHostConfigured
2716
+ ? ''
2717
+ : '\nFalling back to client side rendering. This will become a 400 Bad Request in a future major version.') +
2718
+ '\n\nFor more information, see https://angular.dev/best-practices/security#preventing-server-side-request-forgery-ssrf');
2719
+ if (isAllowedHostConfigured) {
2720
+ // Allowed hosts has been configured incorrectly, thus we can return a 400 bad request.
2721
+ return new Response(errorMessage, {
2722
+ status: 400,
2723
+ statusText: 'Bad Request',
2724
+ headers: { 'Content-Type': 'text/plain' },
2725
+ });
2726
+ }
2727
+ // Fallback to CSR to avoid a breaking change.
2728
+ // TODO(alanagius): Return a 400 and remove this fallback in the next major version (v22).
2729
+ const serverApp = await this.getAngularServerAppForRequest(request);
2730
+ return serverApp?.serveClientSidePage() ?? null;
2731
+ }
2648
2732
  }
2649
2733
 
2650
2734
  /**