Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(@angular/ssr): use join instead of resolve #26205

Merged
merged 1 commit into from Nov 3, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
61 changes: 27 additions & 34 deletions packages/angular/ssr/src/common-engine.ts
Expand Up @@ -9,7 +9,7 @@
import { ApplicationRef, StaticProvider, Type } from '@angular/core';
import { renderApplication, renderModule, ɵSERVER_CONTEXT } from '@angular/platform-server';
import * as fs from 'node:fs';
import { dirname, resolve } from 'node:path';
import { dirname, join, normalize, resolve } from 'node:path';
import { URL } from 'node:url';
import { InlineCriticalCssProcessor, InlineCriticalCssResult } from './inline-css-processor';
import {
Expand Down Expand Up @@ -117,32 +117,34 @@ export class CommonEngine {
return undefined;
}

const pathname = canParseUrl(url) ? new URL(url).pathname : url;
// Remove leading forward slash.
const pagePath = resolve(publicPath, pathname.substring(1), 'index.html');

if (pagePath !== resolve(documentFilePath)) {
// View path doesn't match with prerender path.
const pageIsSSG = this.pageIsSSG.get(pagePath);
if (pageIsSSG === undefined) {
if (await exists(pagePath)) {
const content = await fs.promises.readFile(pagePath, 'utf-8');
const isSSG = SSG_MARKER_REGEXP.test(content);
this.pageIsSSG.set(pagePath, isSSG);

if (isSSG) {
return content;
}
} else {
this.pageIsSSG.set(pagePath, false);
}
} else if (pageIsSSG) {
// Serve pre-rendered page.
return fs.promises.readFile(pagePath, 'utf-8');
}
const { pathname } = new URL(url, 'resolve://');
// Do not use `resolve` here as otherwise it can lead to path traversal vulnerability.
// See: https://portswigger.net/web-security/file-path-traversal
const pagePath = join(publicPath, pathname, 'index.html');

if (this.pageIsSSG.get(pagePath)) {
// Serve pre-rendered page.
return fs.promises.readFile(pagePath, 'utf-8');
}

if (!pagePath.startsWith(normalize(publicPath))) {
alan-agius4 marked this conversation as resolved.
Show resolved Hide resolved
// Potential path traversal detected.
return undefined;
}

if (pagePath === resolve(documentFilePath) || !(await exists(pagePath))) {
// View matches with prerender path or file does not exist.
this.pageIsSSG.set(pagePath, false);

return undefined;
}

return undefined;
// Static file exists.
const content = await fs.promises.readFile(pagePath, 'utf-8');
const isSSG = SSG_MARKER_REGEXP.test(content);
this.pageIsSSG.set(pagePath, isSSG);

return isSSG ? content : undefined;
}

private async renderApplication(opts: CommonEngineRenderOptions): Promise<string> {
Expand Down Expand Up @@ -202,12 +204,3 @@ function isBootstrapFn(value: unknown): value is () => Promise<ApplicationRef> {
// We can differentiate between a module and a bootstrap function by reading compiler-generated `ɵmod` static property:
return typeof value === 'function' && !('ɵmod' in value);
}

// The below can be removed in favor of URL.canParse() when Node.js 18 is dropped
function canParseUrl(url: string): boolean {
try {
return !!new URL(url);
} catch {
return false;
}
}