mirror of https://github.com/garrytan/gstack.git
fix(design): bind server to localhost and validate reload paths
Cherry-pick PR #803 by @garagon. Adds hostname: '127.0.0.1' to Bun.serve() and validates /api/reload paths are within cwd() or tmpdir(). Closes C1+C2 from security audit #783. Co-Authored-By: garagon <garagon@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3113d36d5d
commit
e046e851eb
|
|
@ -33,19 +33,21 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
import os from "os";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { spawn } from "child_process";
|
import { spawn } from "child_process";
|
||||||
|
|
||||||
export interface ServeOptions {
|
export interface ServeOptions {
|
||||||
html: string;
|
html: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
hostname?: string; // default '127.0.0.1' — localhost only
|
||||||
timeout?: number; // seconds, default 600 (10 min)
|
timeout?: number; // seconds, default 600 (10 min)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerState = "serving" | "regenerating" | "done";
|
type ServerState = "serving" | "regenerating" | "done";
|
||||||
|
|
||||||
export async function serve(options: ServeOptions): Promise<void> {
|
export async function serve(options: ServeOptions): Promise<void> {
|
||||||
const { html, port = 0, timeout = 600 } = options;
|
const { html, port = 0, hostname = '127.0.0.1', timeout = 600 } = options;
|
||||||
|
|
||||||
// Validate HTML file exists
|
// Validate HTML file exists
|
||||||
if (!fs.existsSync(html)) {
|
if (!fs.existsSync(html)) {
|
||||||
|
|
@ -59,6 +61,7 @@ export async function serve(options: ServeOptions): Promise<void> {
|
||||||
|
|
||||||
const server = Bun.serve({
|
const server = Bun.serve({
|
||||||
port,
|
port,
|
||||||
|
hostname,
|
||||||
fetch(req) {
|
fetch(req) {
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
|
|
||||||
|
|
@ -182,6 +185,17 @@ export async function serve(options: ServeOptions): Promise<void> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate path is within cwd or temp directory
|
||||||
|
const resolved = path.resolve(newHtmlPath);
|
||||||
|
const safeDirs = [process.cwd(), os.tmpdir()];
|
||||||
|
const isSafe = safeDirs.some(dir => resolved.startsWith(dir + path.sep) || resolved === dir);
|
||||||
|
if (!isSafe) {
|
||||||
|
return Response.json(
|
||||||
|
{ error: `Path must be within working directory or temp` },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Swap the HTML content
|
// Swap the HTML content
|
||||||
htmlContent = fs.readFileSync(newHtmlPath, "utf-8");
|
htmlContent = fs.readFileSync(newHtmlPath, "utf-8");
|
||||||
state = "serving";
|
state = "serving";
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue