Overview

The core idea in SpyBug is to make one uploaded file satisfy two different parsers:

  • The server should recognize it as a WAV file.
  • The browser should be able to parse its contents as JavaScript.

Commenting out the WAV header is the right concept, but passing the upload check does not automatically make the file executable. There are two separate blockers to investigate: the server-side file signature check and the browser’s MIME-type enforcement.

Building a WAV/JavaScript polyglot

A simple WAV detector generally looks for these bytes:

RIFF....WAVE

RIFF must appear at offset 0, while WAVE must appear at offset 8. The four bytes between them normally store the RIFF chunk size, but a weak signature-based validator may only care about the two magic values.

This gives us room to make the same prefix valid JavaScript:

RIFF=0/*WAVE*/;console.log("loaded")

The detector sees:

RIFF  =0/*  WAVE
^^^^        ^^^^
0x00        0x08

The JavaScript engine sees:

RIFF = 0 /* WAVE */;
console.log("loaded");

Here, RIFF=0 is a valid assignment, /* starts a block comment, and the WAVE marker is hidden inside that comment. The closing */ lets the remaining JavaScript execute normally.

Create a small test payload locally:

payload = b'RIFF=0/*WAVE*/;console.log("loaded")'

with open("test.wav", "wb") as f:
    f.write(payload)

Inspect the resulting bytes and see how the operating system identifies the file:

xxd test.wav
file test.wav

The important test, however, is to use the exact Node.js library and version used by the challenge. For example:

const FileType = require("file-type");

(async () => {
  console.log(await FileType.fromFile("test.wav"));
})();

If the result is equivalent to the following, the validator is relying on the RIFF/WAVE signature instead of requiring a fully valid audio file:

{ ext: "wav", mime: "audio/vnd.wave" }

At this point, the polyglot has passed the upload-side check. That only solves the first half of the problem.

The second blocker: script MIME type

The uploaded file may be stored at a URL such as:

/uploads/9570384f-d6a5-439f-951e-53851d008ee6

Notice that the generated path has no .wav extension. If Express serves this file with the following headers:

Content-Type: application/octet-stream
X-Content-Type-Options: nosniff

then loading it as a script will fail:

<script src="/uploads/9570384f-d6a5-439f-951e-53851d008ee6"></script>

With nosniff enabled, the browser trusts the declared Content-Type and refuses to reinterpret an application/octet-stream response as JavaScript. The payload can therefore be valid JavaScript and still never reach the JavaScript parser.

Check the response headers directly:

curl -I http://localhost:1337/uploads/9570384f-d6a5-439f-951e-53851d008ee6

Also inspect the browser console. A strict MIME-type failure should produce an error similar to:

Refused to execute script because its MIME type ('application/octet-stream')
is not executable, and strict MIME type checking is enabled.

Debugging checklist

Treat the exploit as two independent questions:

  1. Does the challenge’s file-type library accept the crafted RIFF/WAVE prefix?
  2. Does the upload endpoint serve the stored file with a JavaScript-compatible MIME type?

If the first answer is no, adjust the polyglot for the exact validator. If the first answer is yes but the browser reports a strict MIME-type error, the remaining obstacle is the response headers—not the WAV/JavaScript syntax.

Takeaway

The sequence RIFF=0/*WAVE*/; is a useful way to explore weak WAV signature validation because it places both required magic values at the correct offsets while remaining valid JavaScript. However, upload validation and browser execution are separate security boundaries. Passing the server’s file-type check is not enough when X-Content-Type-Options: nosniff and a non-script Content-Type prevent the browser from executing the uploaded response.