Browse Source

fix: add security guards and type consistency for PR #71

- Add localhost-only guard to /api/open-file (rejects non-localhost with 403)
- Restrict open-file paths to user's home directory
- Change savedFilename/savedFilePath from optional to required-but-nullable
- Remove zip from KNOWN_3D_EXTENSIONS (not a valid 3D format)
- Export getExtensionFromUrl for testability

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
b8d31b0376
  1. 35
      src/app/api/open-file/route.ts
  2. 4
      src/app/api/save-generation/route.ts
  3. 4
      src/types/nodes.ts

35
src/app/api/open-file/route.ts

@ -7,7 +7,33 @@ import os from "os";
const execFileAsync = promisify(execFile);
function isLocalhostRequest(req: NextRequest): boolean {
const forwarded = req.headers.get("x-forwarded-for");
if (forwarded) {
const firstIp = forwarded.split(",")[0].trim();
if (firstIp !== "127.0.0.1" && firstIp !== "::1" && firstIp !== "::ffff:127.0.0.1") {
return false;
}
}
const host = req.headers.get("host") || "";
const hostname = host.split(":")[0];
if (hostname && hostname !== "localhost" && hostname !== "127.0.0.1" && hostname !== "::1") {
return false;
}
return true;
}
export async function POST(req: NextRequest) {
// Only allow requests from localhost
if (!isLocalhostRequest(req)) {
return NextResponse.json(
{ success: false, error: "Forbidden: localhost only" },
{ status: 403 }
);
}
try {
const body = await req.json();
const { filePath: inputPath } = body;
@ -22,6 +48,15 @@ export async function POST(req: NextRequest) {
// Normalize and resolve the path to prevent traversal attacks
const normalizedPath = path.resolve(inputPath);
// Restrict to user's home directory
const homeDir = os.homedir();
if (!normalizedPath.startsWith(homeDir + path.sep) && normalizedPath !== homeDir) {
return NextResponse.json(
{ success: false, error: "Path is outside allowed directory" },
{ status: 403 }
);
}
// Validate that the path exists and is a file
try {
const stats = await stat(normalizedPath);

4
src/app/api/save-generation/route.ts

@ -49,11 +49,11 @@ function isHttpUrl(str: string): boolean {
}
// Known file extensions for 3D models and common media
const KNOWN_3D_EXTENSIONS = new Set(["glb", "gltf", "obj", "fbx", "usdz", "stl", "ply", "zip"]);
const KNOWN_3D_EXTENSIONS = new Set(["glb", "gltf", "obj", "fbx", "usdz", "stl", "ply"]);
const KNOWN_MEDIA_EXTENSIONS = new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "mp4", "webm", "mov"]);
// Helper to extract a recognized file extension from a URL pathname
function getExtensionFromUrl(url: string): string | null {
export function getExtensionFromUrl(url: string): string | null {
try {
const urlObj = new URL(url);
const pathname = urlObj.pathname;

4
src/types/nodes.ts

@ -183,8 +183,8 @@ export interface Generate3DNodeData extends BaseNodeData {
inputImageRefs?: string[];
inputPrompt: string | null;
output3dUrl: string | null;
savedFilename?: string | null;
savedFilePath?: string | null;
savedFilename: string | null;
savedFilePath: string | null;
selectedModel?: SelectedModel;
parameters?: Record<string, unknown>;
inputSchema?: ModelInputDef[];

Loading…
Cancel
Save