diff --git a/server/routes.ts b/server/routes.ts index fdbcbc2..429a410 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -9,6 +9,7 @@ import { registerChatRoutes } from "./replit_integrations/chat"; import { registerImageRoutes } from "./replit_integrations/image"; import { aiRateLimiter, writeRateLimiter } from "./middleware"; import OpenAI from "openai"; +import { verifyAudioFileSignature, sanitizeLog } from "./utils"; // Helper to validate numeric IDs from route params function parseNumericId(value: string, res: Response): number | null { @@ -1147,6 +1148,17 @@ Also suggest a fitting title for the song.`; return res.status(400).json({ message: "Reference audio file is required" }); } + // Verify file signature (magic bytes) + if (!verifyAudioFileSignature(file.buffer)) { + console.error("File signature validation failed:", sanitizeLog({ + userId: req.user.claims.sub, + fileSize: file.size, + mimeType: file.mimetype, + originalName: file.originalname + })); + return res.status(400).json({ message: "Invalid file signature. Please upload a valid audio file." }); + } + const { prompt, duration } = req.body; if (!prompt || typeof prompt !== "string" || prompt.trim().length === 0) { return res.status(400).json({ message: "Prompt is required" }); diff --git a/server/utils.test.ts b/server/utils.test.ts index 89c5ee4..8e36186 100644 --- a/server/utils.test.ts +++ b/server/utils.test.ts @@ -1,9 +1,9 @@ -import { sanitizeLog } from "./utils"; +import { sanitizeLog, verifyAudioFileSignature } from "./utils"; import assert from "assert"; console.log("Running sanitization tests..."); -const testCases = [ +const sanitizationTestCases = [ { name: "Redact email", input: { id: 1, email: "test@example.com" }, @@ -40,11 +40,16 @@ const testCases = [ input: { val: null, other: undefined }, expected: { val: null, other: undefined }, }, + { + name: "Prevent log injection with newlines", + input: { filename: "test\ninjection\r\nattack.txt" }, + expected: { filename: "testinjectionattack.txt" }, + }, ]; let failed = false; -for (const test of testCases) { +for (const test of sanitizationTestCases) { try { const result = sanitizeLog(test.input); assert.deepStrictEqual(result, test.expected); @@ -57,9 +62,137 @@ for (const test of testCases) { } } +console.log("\nRunning file signature tests..."); + +const signatureTestCases = [ + { + name: "Valid MP3 (ID3)", + input: Buffer.from([0x49, 0x44, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + expected: true, + }, + { + name: "Valid MP3 (Sync Frame)", + input: Buffer.from([0xFF, 0xFB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + expected: true, + }, + { + name: "Valid MP3 (MPEG-1 Layer III - FF FA)", + input: Buffer.from([0xFF, 0xFA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + expected: true, + }, + { + name: "Valid MP3 (MPEG-2 Layer III - FF F2)", + input: Buffer.from([0xFF, 0xF2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + expected: true, + }, + { + name: "Valid MP3 (MPEG-2.5 Layer III - FF E3)", + input: Buffer.from([0xFF, 0xE3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + expected: true, + }, + { + name: "Valid MP3 (MPEG-2.5 Layer III - FF E2)", + input: Buffer.from([0xFF, 0xE2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + expected: true, + }, + { + name: "Valid WAV", + input: Buffer.from([0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x57, 0x41, 0x56, 0x45]), + expected: true, + }, + { + name: "Valid OGG", + input: Buffer.from([0x4F, 0x67, 0x67, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + expected: true, + }, + { + name: "Valid FLAC", + input: Buffer.from([0x66, 0x4C, 0x61, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + expected: true, + }, + { + name: "Valid M4A (M4A brand)", + input: Buffer.from([0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x4D, 0x34, 0x41, 0x20]), // ftypM4A + expected: true, + }, + { + name: "Valid M4A (M4B brand)", + input: Buffer.from([0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x4D, 0x34, 0x42, 0x20]), // ftypM4B + expected: true, + }, + { + name: "Valid M4B (ftyp)", + input: Buffer.from([0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70, 0x4D, 0x34, 0x42, 0x20]), + expected: true, + }, + { + name: "Valid M4P (ftyp)", + input: Buffer.from([0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70, 0x4D, 0x34, 0x50, 0x20]), + expected: true, + }, + { + name: "Invalid MP4 video (ftyp + isom brand)", + input: Buffer.from([0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6F, 0x6D]), + expected: false, + }, + { + name: "Valid AAC (ADTS)", + input: Buffer.from([0xFF, 0xF1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + expected: true, + }, + { + name: "Valid AAC (ADTS MPEG-2)", + input: Buffer.from([0xFF, 0xF9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + expected: true, + }, + { + name: "Invalid Text File", + input: Buffer.from("This is a text file content"), + expected: false, + }, + { + name: "Invalid EXE (MZ header)", + input: Buffer.from([0x4D, 0x5A, 0x90, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00]), + expected: false, + }, + { + name: "Invalid MP4 Video (isom brand)", + input: Buffer.from([0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6F, 0x6D]), // ftypisom + expected: false, + }, + { + name: "Invalid UTF-16 LE BOM (ADTS false positive check)", + input: Buffer.from([0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + expected: false, + }, + { + name: "Empty Buffer", + input: Buffer.alloc(0), + expected: false, + }, + { + name: "Short Buffer", + input: Buffer.from([0xFF, 0xFB]), + expected: false, + }, +]; + +for (const test of signatureTestCases) { + try { + const result = verifyAudioFileSignature(test.input); + assert.strictEqual(result, test.expected); + console.log(`✅ ${test.name}`); + } catch (err) { + console.error(`❌ ${test.name} FAILED`); + console.error("Expected:", test.expected); + console.error("Actual:", verifyAudioFileSignature(test.input)); + failed = true; + } +} + if (failed) { - console.error("Some tests failed."); + console.error("\nSome tests failed."); process.exit(1); } else { - console.log("All tests passed!"); + console.log("\nAll tests passed!"); } diff --git a/server/utils.ts b/server/utils.ts index 4f3952f..2e50870 100644 --- a/server/utils.ts +++ b/server/utils.ts @@ -32,6 +32,9 @@ export function sanitizeLog(data: any): any { // Check if key matches any sensitive pattern if (SENSITIVE_PATTERNS.some((pattern) => pattern.test(key))) { sanitized[key] = "***REDACTED***"; + } else if (typeof sanitized[key] === "string") { + // Prevent log injection by removing newlines from strings + sanitized[key] = sanitized[key].replace(/[\r\n]/g, ''); } else if (typeof sanitized[key] === "object" && sanitized[key] !== null) { // Recursively sanitize objects sanitized[key] = sanitizeLog(sanitized[key]); @@ -40,3 +43,59 @@ export function sanitizeLog(data: any): any { return sanitized; } + +/** + * Validates audio file signatures to prevent malicious uploads. + * Checks for MP3, WAV, OGG, FLAC, and AAC/M4A magic bytes. + */ +export function verifyAudioFileSignature(buffer: Buffer): boolean { + if (!buffer || buffer.length < 12) return false; + + const header = buffer.subarray(0, 12); + + // Magic bytes constants + const SIGNATURES = { + ID3: Buffer.from([0x49, 0x44, 0x33]), + RIFF: Buffer.from([0x52, 0x49, 0x46, 0x46]), + WAVE: Buffer.from([0x57, 0x41, 0x56, 0x45]), + OGG: Buffer.from([0x4F, 0x67, 0x67, 0x53]), + FLAC: Buffer.from([0x66, 0x4C, 0x61, 0x43]), + FTYP: Buffer.from([0x66, 0x74, 0x79, 0x70]), + }; + + // MP3: ID3v2 tag or Sync Frame. + // Sync frame: FF Fx (MPEG-1 Layer III: FF FB/FA, MPEG-2 Layer III: FF F3/F2, MPEG-2.5 Layer III: FF E3/E2) + // We explicitly check for Layer III to avoid false positives like UTF-16 LE BOM (FF FE) which looks like MPEG-1 Layer I. + const isMp3 = + header.subarray(0, 3).equals(SIGNATURES.ID3) || + (header[0] === 0xff && ( + header[1] === 0xfb || header[1] === 0xfa || // MPEG-1 Layer III + header[1] === 0xf3 || header[1] === 0xf2 || // MPEG-2 Layer III + header[1] === 0xe3 || header[1] === 0xe2 // MPEG-2.5 Layer III + )); + + // WAV: RIFF header with WAVE format + const isWav = + header.subarray(0, 4).equals(SIGNATURES.RIFF) && + header.subarray(8, 12).equals(SIGNATURES.WAVE); + + // OGG: OggS capture pattern + const isOgg = header.subarray(0, 4).equals(SIGNATURES.OGG); + + // FLAC: fLaC marker + const isFlac = header.subarray(0, 4).equals(SIGNATURES.FLAC); + + // M4A/MP4: ftyp box at offset 4 AND valid audio brand + // Brands: M4A, M4B, M4P + const isM4a = + header.subarray(4, 8).equals(SIGNATURES.FTYP) && + (header.subarray(8, 12).equals(Buffer.from("M4A ")) || + header.subarray(8, 12).equals(Buffer.from("M4B ")) || + header.subarray(8, 12).equals(Buffer.from("M4P "))); + + // AAC ADTS: Sync word (12 bits of 1s) + // header[0] == 0xFF, header[1] & 0xF6 == 0xF0 (MPEG-4: F1, MPEG-2: F9, valid protection/layer bits) + const isAacAdts = header[0] === 0xff && (header[1] & 0xf6) === 0xf0; + + return isMp3 || isWav || isOgg || isFlac || isM4a || isAacAdts; +}