mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-22 09:35:10 +02:00
fix(skills): confine frontend-slides export server to the deck directory and bind loopback (#3206)
The temporary HTTP server that export-pdf.sh spins up to render a deck joined the decoded request path onto SERVE_DIR without checking the resolved path, and listened on all interfaces. A request such as `/..%2f..%2f..%2fetc%2fpasswd` (raw, or issued by a malicious deck via fetch() while it renders) returned files outside the deck directory, and any host on the network could hit the port while an export was running. - resolve the decoded path against SERVE_ROOT and answer 403 when the result is not inside it (handles ../, %2e%2e, %2f and query strings) - answer 400 on malformed percent-encoding instead of throwing - listen on 127.0.0.1 only; the only client is the local headless browser Verified with raw HTTP requests against the handler: `/` and `/index.html` -> 200, `/../x`, `/..%2f..%2fx`, `/%2e%2e/x`, `/deck/../../x` -> 403, unknown file -> 404, `/%zz` -> 400, and server.address() reports 127.0.0.1. Fixes #3101
This commit is contained in:
@@ -136,7 +136,7 @@ cat > "$TEMP_SCRIPT" << 'EXPORT_SCRIPT'
|
||||
import { chromium } from 'playwright';
|
||||
import { createServer } from 'http';
|
||||
import { readFileSync, existsSync, mkdirSync, unlinkSync, writeFileSync } from 'fs';
|
||||
import { join, extname, resolve } from 'path';
|
||||
import { join, extname, resolve, sep } from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const SERVE_DIR = process.argv[2];
|
||||
@@ -166,10 +166,25 @@ const MIME_TYPES = {
|
||||
'.eot': 'application/vnd.ms-fontobject',
|
||||
};
|
||||
|
||||
// Every request is confined to the deck directory: resolve the decoded path
|
||||
// against SERVE_ROOT and refuse anything that escapes it (../, %2e%2e, %2f).
|
||||
const SERVE_ROOT = resolve(SERVE_DIR);
|
||||
const server = createServer((req, res) => {
|
||||
// Decode URL-encoded characters (e.g., %20 -> space) so filenames with spaces resolve correctly
|
||||
const decodedUrl = decodeURIComponent(req.url);
|
||||
let filePath = join(SERVE_DIR, decodedUrl === '/' ? HTML_FILE : decodedUrl);
|
||||
let decodedUrl;
|
||||
try {
|
||||
decodedUrl = decodeURIComponent((req.url || '/').split('?')[0]);
|
||||
} catch {
|
||||
res.writeHead(400);
|
||||
res.end('Bad request');
|
||||
return;
|
||||
}
|
||||
const filePath = resolve(SERVE_ROOT, '.' + (decodedUrl === '/' ? '/' + HTML_FILE : decodedUrl));
|
||||
if (filePath !== SERVE_ROOT && !filePath.startsWith(SERVE_ROOT + sep)) {
|
||||
res.writeHead(403);
|
||||
res.end('Forbidden');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const content = readFileSync(filePath);
|
||||
const ext = extname(filePath).toLowerCase();
|
||||
@@ -181,9 +196,9 @@ const server = createServer((req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Find a free port
|
||||
// Find a free port on loopback only; the deck is rendered by the local headless browser
|
||||
const port = await new Promise((resolve) => {
|
||||
server.listen(0, () => resolve(server.address().port));
|
||||
server.listen(0, '127.0.0.1', () => resolve(server.address().port));
|
||||
});
|
||||
|
||||
console.log(` Local server on port ${port}`);
|
||||
|
||||
Reference in New Issue
Block a user