Skip to content

v2.1.2

Choose a tag to compare

@github-actions github-actions released this 06 Nov 06:53
· 42 commits to main since this release
v2.1.2
c8e3592

WaveFlow Server 2.1.2

A security release that completes 2.1.1. The check 2.1.1 added against a crafted audio file could be walked around by a WAV built for the purpose, and it refused some large files that were perfectly sound. Both are fixed here, and nothing else changes, so upgrade as soon as you can.

Security advisory: GHSA-6qhc-763f-95v3.

Security

  • A crafted WAV can no longer carry nested chapter frames past the check. 2.1.1 refuses a tag whose ID3v2 chapter frames nest, before the tag reader meets it, because that reader recurses without limit and a nested tag of a few kilobytes aborts the whole process. In a WAV, the check found the tag by walking the file's chunks the way it expected the reader to walk them. The reader does not always walk them that way.

    A WAV can hold a LIST INFO chunk, a list of text items such as a title or an artist. The reader goes through those items one by one, and when it stops partway — at an item whose name is not a valid one, or whose length runs past the list — it carries on with the rest of the file from where it stopped, not from the end of the list. A chunk header placed at that exact spot could then send the reader anywhere in the file, onto a tag the check never looked at, since the check had gone on from the end of the list.

    This was measured on 2.1.1 itself, with nine files built for the purpose. The server's own call to the reader read the tag in all nine, and the check let the crafted twin through in six. What those measurements show is the tag reaching the parser that 2.1.1 protects, through the same call every tag in a WAV goes through. No process was crashed along this path: the crash is the one the 2.1.1 notes describe.

    The file arrives the same two ways as before: sent by an account allowed to upload to a library, or placed in a scanned folder. 2.1.1 is affected. Releases before it are affected by the original flaw, which they do not check for at all.

    The check now works out every place the reader carries on from after such a list, and looks there too. Each of those places is held by a test that reads the same file through the reader itself, so a change in the reader fails the tests. A sweep of 200,000 malformed files, built from fixed seeds, runs with them. Each file is built twice around the same tag: the reader is given the copy whose tag it rejects with an error of its own, which shows it tried to parse that tag, and the part of the check that walks a WAV's or an AIFF's chunks is given the copy with nested chapter frames. No file whose tag the reader tries to parse may pass that walk. The fix is #431.

  • A file that cannot be read through is refused. When a read failed partway through a file, 2.1.1 took the failure for the end of a clean file and let it through, while the tag reader could read further and still meet nested chapter frames. Such a file is now refused: a scan records an error for it, and an upload is answered 422. A read that is merely interrupted is tried again, so a healthy file is not refused for it. This holds in a DSF or DFF file too, including for the few header bytes the check reads to find where such a file keeps its tag. A file that simply ends early is not a failed read, and passes as before: the reader meets the same end. The fixes are #429, #430 and #433.

Fixed

  • Large files that nest nothing are no longer refused. This is the known limit 2.1.1 shipped with. Its check gave up on a file, and refused it, once it had read 64 MiB of it, and two sound shapes of file made it read that much:

    • a WAV or an AIFF that carries a chunk of odd length ahead of its audio;
    • a FLAC that keeps the chunks of the WAV it was made from (flac --keep-foreign-metadata), or any other file with the letters RIFF or FORM in its first 64 KiB.

    The check no longer reads the audio in either. It walks chunks only in a file that opens as a WAV or an AIFF, and in those it reads the tags and nothing else, as the reader does. Measured on built files of 100 MiB to 3.5 GiB, 1,600 sizes for each way a writer pads an odd chunk: none refused.

    After the upgrade, a file 2.1.1 refused is read again by the next scan, as a refused file is by every scan, and is then indexed like any other. The fixes are #430 and #431, for #428.

  • A refusal says what it is. 2.1.1 gave one reason for every file it refused, nested chapter frames, even when it had found none. The reason now tells three cases apart: the tag nests chapter frames, the file's metadata runs past what the check will read, or the file could not be read through its tags.

  • A full rescan keeps the track of a refused file. When a full rescan meets a file the check refuses, the track is marked unavailable rather than deleted. The file is untouched, and a later scan that reads it brings the track back.

Known limit: a file that changes while it is read

The check reads a file, and the tag reader then opens it again. A file that changes between the two, such as one still being copied into a scanned folder, is read by the reader as it then is, not as the check saw it. Where the check met the end of the file ahead of a tag, it found nothing to refuse, and the reader may find a tag there a moment later.

This has been so since 2.1.1. The check and the tag reader each open the file and read it at their own moment, so a change between those two reads cannot be ruled out. It takes write access to the library folder and a change that lands between the two reads. Uploading a file does not open that window by itself: the server receives the whole file, verifies its hash and moves it into place before reading it, so it never reads a file that is still arriving. That does not protect the file once it is in the folder. Someone who can write there can change an uploaded file like any other. Once the file is whole, every later read of it goes through the check again, the scan at the next start included, so such a file does not keep the server from starting. This was read from the code, not measured.

Not verified

  • No process was crashed through the new path. The measurements stop at the tag reaching the parser.
  • Every file measured was built for the purpose. None came from a real library.
  • No read failure was provoked on a real disk. The tests fail a read through a reader built for that.
  • The check follows the tag reader as it is today, lofty 0.25.5. The tests read through that reader, so a later version that walks a file differently fails them rather than passing unnoticed.
  • The sweep of malformed files covers the shapes its generator builds, and that no file passed is evidence over those files, not a proof.
  • Upgrading a library that holds a file 2.1.1 refused was not run end to end.

If you cannot upgrade yet

  • Turn uploads off on every library, or leave upload rights only with people you trust.
  • Keep the folders your libraries scan writable only by people and programs you trust. A file placed there directly is read by the next scan, whatever the upload setting.
  • If your server already crashes at every start, remove the file from the library folder. It is usually the audio file uploaded or added last.

Install

docker run -d --name waveflow \
  -p 4533:4533 \
  -v waveflow-data:/data \
  -v /path/to/music:/music:ro \
  ghcr.io/instazdll/waveflow-server:2.1.2

The image runs on linux/amd64 and linux/arm64, and is the same, digest for digest, on Docker Hub as instazdll/waveflow-server:2.1.2. latest now follows this release. The standalone archives for Windows, macOS and Linux are attached below, with SHA256SUMS over all of them.

Upgrading from 2.1.1

Pull 2.1.2 and start it on the same data volume. With an archive, use the new folder in place of the old one, carrying over what you changed in laptop.env: your data is kept apart from that folder. The database schema has not changed since 2.1.0, so there is nothing to migrate, and 2.1.1 still opens the same data should you need to go back.

Coming from 2.0.1 or earlier, read the 2.1.0 notes first: that upgrade changes the schema and measures every lossless file again.

Full changelog: CHANGELOG.md · v2.1.1...v2.1.2

Download

The server for one computer, without Docker: extract the archive and start waveflow-server inside it. Each archive holds the server, its own FFmpeg and a README.txt to read first.

System Archive
Windows (x86_64) waveflow-server-2.1.2-windows-x86_64.zip
macOS (Apple silicon) waveflow-server-2.1.2-macos-arm64.zip
macOS (Intel) waveflow-server-2.1.2-macos-x86_64.zip
Linux (x86_64) waveflow-server-2.1.2-linux-x86_64.tar.gz
Linux (arm64) waveflow-server-2.1.2-linux-arm64.tar.gz

The Windows executables carry a self-signed certificate and the macOS ones an ad-hoc signature, which neither system trusts, so both warn on the first launch: README.txt says what to click past. SHA256SUMS holds the checksums of them all, and ffmpeg-8.1.3-sources.tar.gz the source of the FFmpeg they carry.

Container image

docker pull ghcr.io/instazdll/waveflow-server:2.1.2

The same image, digest for digest, is on Docker Hub:

docker pull instazdll/waveflow-server:2.1.2

Also published as latest in both, for linux/amd64 and linux/arm64 (a Raspberry Pi 4 or 5 on a 64-bit system).