Skip to content
12 changes: 12 additions & 0 deletions server/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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" });
Expand Down
143 changes: 138 additions & 5 deletions server/utils.test.ts
Original file line number Diff line number Diff line change
@@ -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" },
Expand Down Expand Up @@ -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);
Expand All @@ -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,
},
];
Comment on lines 67 to 178
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test case for rejecting generic MP4 video files (e.g., files with 'ftyp' followed by 'isom' brand). This is critical for verifying the M4A validation doesn't allow MP4 videos to pass. Add a test with Buffer containing [0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6F, 0x6D] (ftyp + isom brand) and expect it to return false.

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated the tests to include UTF-16 LE BOM and generic MP4 video cases as requested.


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!");
}
59 changes: 59 additions & 0 deletions server/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand All @@ -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.
Comment on lines 47 to 49
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment uses "Sentinel πŸ›‘οΈ:" as a branded prefix, which violates the coding guidelines. According to the project guidelines, comments should not use branded prefixes. Remove the "Sentinel πŸ›‘οΈ:" prefix and use a simple descriptive comment instead, such as "Validates audio file signatures to prevent malicious uploads."

Copilot generated this review using guidance from repository custom instructions.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

*/
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;
}
Comment on lines 51 to 101
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The function verifyAudioFileSignature is well-structured and covers many important audio formats. However, it uses many "magic numbers" directly in the if conditions, which can make the code harder to read and maintain.

To improve clarity and maintainability, I suggest refactoring this function to use named constants for the magic byte sequences and Buffer.equals() for comparisons. This makes the purpose of each check more explicit and the code more idiomatic. The SIGNATURES object is defined inside the function for a self-contained suggestion, but could be moved to the module scope for a minor performance gain.

export function verifyAudioFileSignature(buffer: Buffer): boolean {
  if (!buffer || buffer.length < 12) return false;

  const header = buffer.subarray(0, 12);

  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. A more general check for MPEG frames is used.
  const isMp3 =
    header.subarray(0, 3).equals(SIGNATURES.ID3) ||
    (header[0] === 0xff && (header[1] & 0xe0) === 0xe0);

  // 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
  const isM4a = header.subarray(4, 8).equals(SIGNATURES.FTYP);

  // AAC ADTS: Sync word (12 bits of 1s)
  const isAacAdts = header[0] === 0xff && (header[1] & 0xf0) === 0xf0;

  return isMp3 || isWav || isOgg || isFlac || isM4a || isAacAdts;
}