|
9 | 9 | import { ApplicationRef, StaticProvider, Type } from '@angular/core'; |
10 | 10 | import { renderApplication, renderModule, ɵSERVER_CONTEXT } from '@angular/platform-server'; |
11 | 11 | import * as fs from 'node:fs'; |
12 | | -import { dirname, resolve } from 'node:path'; |
| 12 | +import { dirname, join, normalize, resolve } from 'node:path'; |
13 | 13 | import { URL } from 'node:url'; |
14 | 14 | import { InlineCriticalCssProcessor, InlineCriticalCssResult } from './inline-css-processor'; |
15 | 15 | import { |
@@ -117,32 +117,34 @@ export class CommonEngine { |
117 | 117 | return undefined; |
118 | 118 | } |
119 | 119 |
|
120 | | - const pathname = canParseUrl(url) ? new URL(url).pathname : url; |
121 | | - // Remove leading forward slash. |
122 | | - const pagePath = resolve(publicPath, pathname.substring(1), 'index.html'); |
123 | | - |
124 | | - if (pagePath !== resolve(documentFilePath)) { |
125 | | - // View path doesn't match with prerender path. |
126 | | - const pageIsSSG = this.pageIsSSG.get(pagePath); |
127 | | - if (pageIsSSG === undefined) { |
128 | | - if (await exists(pagePath)) { |
129 | | - const content = await fs.promises.readFile(pagePath, 'utf-8'); |
130 | | - const isSSG = SSG_MARKER_REGEXP.test(content); |
131 | | - this.pageIsSSG.set(pagePath, isSSG); |
132 | | - |
133 | | - if (isSSG) { |
134 | | - return content; |
135 | | - } |
136 | | - } else { |
137 | | - this.pageIsSSG.set(pagePath, false); |
138 | | - } |
139 | | - } else if (pageIsSSG) { |
140 | | - // Serve pre-rendered page. |
141 | | - return fs.promises.readFile(pagePath, 'utf-8'); |
142 | | - } |
| 120 | + const { pathname } = new URL(url, 'resolve://'); |
| 121 | + // Do not use `resolve` here as otherwise it can lead to path traversal vulnerability. |
| 122 | + // See: https://portswigger.net/web-security/file-path-traversal |
| 123 | + const pagePath = join(publicPath, pathname, 'index.html'); |
| 124 | + |
| 125 | + if (this.pageIsSSG.get(pagePath)) { |
| 126 | + // Serve pre-rendered page. |
| 127 | + return fs.promises.readFile(pagePath, 'utf-8'); |
| 128 | + } |
| 129 | + |
| 130 | + if (!pagePath.startsWith(normalize(publicPath))) { |
| 131 | + // Potential path traversal detected. |
| 132 | + return undefined; |
| 133 | + } |
| 134 | + |
| 135 | + if (pagePath === resolve(documentFilePath) || !(await exists(pagePath))) { |
| 136 | + // View matches with prerender path or file does not exist. |
| 137 | + this.pageIsSSG.set(pagePath, false); |
| 138 | + |
| 139 | + return undefined; |
143 | 140 | } |
144 | 141 |
|
145 | | - return undefined; |
| 142 | + // Static file exists. |
| 143 | + const content = await fs.promises.readFile(pagePath, 'utf-8'); |
| 144 | + const isSSG = SSG_MARKER_REGEXP.test(content); |
| 145 | + this.pageIsSSG.set(pagePath, isSSG); |
| 146 | + |
| 147 | + return isSSG ? content : undefined; |
146 | 148 | } |
147 | 149 |
|
148 | 150 | private async renderApplication(opts: CommonEngineRenderOptions): Promise<string> { |
@@ -202,12 +204,3 @@ function isBootstrapFn(value: unknown): value is () => Promise<ApplicationRef> { |
202 | 204 | // We can differentiate between a module and a bootstrap function by reading compiler-generated `ɵmod` static property: |
203 | 205 | return typeof value === 'function' && !('ɵmod' in value); |
204 | 206 | } |
205 | | - |
206 | | -// The below can be removed in favor of URL.canParse() when Node.js 18 is dropped |
207 | | -function canParseUrl(url: string): boolean { |
208 | | - try { |
209 | | - return !!new URL(url); |
210 | | - } catch { |
211 | | - return false; |
212 | | - } |
213 | | -} |
0 commit comments