( req: http.IncomingMessage, res: http.ServerResponse, webDist: string, routes: CompiledRoute[], )
| 134 | } |
| 135 | |
| 136 | async function handleRequest( |
| 137 | req: http.IncomingMessage, |
| 138 | res: http.ServerResponse, |
| 139 | webDist: string, |
| 140 | routes: CompiledRoute[], |
| 141 | ): Promise<void> { |
| 142 | // Don't parse via `new URL()` — its WHATWG normalization collapses `/../foo` to `/foo`, |
| 143 | // hiding traversal attempts before our guard runs. Strip the query string ourselves. |
| 144 | const fullUrl = req.url ?? "/"; |
| 145 | const queryIdx = fullUrl.indexOf("?"); |
| 146 | const pathname = (queryIdx >= 0 ? fullUrl.slice(0, queryIdx) : fullUrl) || "/"; |
| 147 | const method = (req.method ?? "GET").toUpperCase(); |
| 148 | |
| 149 | if (pathname.startsWith("/api/")) { |
| 150 | for (const route of routes) { |
| 151 | if (route.method !== method) continue; |
| 152 | const match = route.regex.exec(pathname); |
| 153 | if (!match) continue; |
| 154 | const params: RouteParams = {}; |
| 155 | try { |
| 156 | route.paramNames.forEach((name, i) => { |
| 157 | params[name] = decodeURIComponent(match[i + 1] ?? ""); |
| 158 | }); |
| 159 | } catch { |
| 160 | res.writeHead(400, { "Content-Type": "text/plain" }); |
| 161 | res.end("Bad Request"); |
| 162 | return; |
| 163 | } |
| 164 | await route.handler(req, res, params); |
| 165 | return; |
| 166 | } |
| 167 | res.writeHead(404, { "Content-Type": "text/plain" }); |
| 168 | res.end("Not Found"); |
| 169 | return; |
| 170 | } |
| 171 | |
| 172 | if (method !== "GET") { |
| 173 | res.writeHead(405, { "Content-Type": "text/plain", Allow: "GET" }); |
| 174 | res.end("Method Not Allowed"); |
| 175 | return; |
| 176 | } |
| 177 | |
| 178 | let decoded: string; |
| 179 | try { |
| 180 | decoded = decodeURIComponent(pathname); |
| 181 | } catch { |
| 182 | res.writeHead(400, { "Content-Type": "text/plain" }); |
| 183 | res.end("Bad Request"); |
| 184 | return; |
| 185 | } |
| 186 | |
| 187 | // path.relative + a check for `..`/absolute is a CodeQL-recognized path-injection sanitizer. |
| 188 | // Building filePath from the validated relative makes the data flow explicit. |
| 189 | const rel = path.relative(webDist, path.resolve(webDist, `.${decoded}`)); |
| 190 | if (rel.startsWith("..") || path.isAbsolute(rel)) { |
| 191 | res.writeHead(403, { "Content-Type": "text/plain" }); |
| 192 | res.end("Forbidden"); |
| 193 | return; |
no test coverage detected