Skip to content

Commit

Permalink
refactor(@angular/ssr): guard against potential path traversals
Browse files Browse the repository at this point in the history
This change updates to code to guard against a potential path traversal.

More context about the reasoning behind this change can be found in https://buganizer.corp.google.com/issues/299878755#comment26

(cherry picked from commit 0f5fb09)
  • Loading branch information
alan-agius4 committed Nov 3, 2023
1 parent b5edaa0 commit 7f9f044
Showing 1 changed file with 27 additions and 34 deletions.
61 changes: 27 additions & 34 deletions packages/angular/ssr/src/common-engine.ts
Original file line number Diff line number Diff line change
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))) {
// 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;
}
}

0 comments on commit 7f9f044

Please sign in to comment.