Skip to content

Security: Nokikuji/NokiPod

SECURITY.md

Security

NokiPod is a v0.1 beta written and maintained by one person. This file says what that means in practice, so you know what to expect before you send a report.

Supported versions

Version Supported
0.1.x Yes, this is the only released line
Anything older, or a build from main No

There is no long-term support branch. Fixes land on main and go out in the next tagged release.

Reporting a vulnerability

Please report privately, not in a public issue.

Open the repository's Security tab and choose Report a vulnerability. That starts a GitHub private security advisory, which only you and the maintainer can see, and it keeps the details out of public view until there is something to update to.

There is no security@ address. One person cannot staff a mailbox, and GitHub's private reporting is the honest equivalent.

What helps most in a report:

  • what an attacker actually gets out of it
  • the version or commit you tested, and how you ran it (AppImage, Windows installer, or from source)
  • the smallest steps that reproduce it

What to expect

No response time is promised here, because none could be guaranteed. What is realistic:

  • an acknowledgement inside about a week
  • for anything that lets someone reach your files or your Last.fm session, a fix in the next release rather than a scheduled one
  • credit in the release notes if you want it, and none if you would rather not

If two weeks pass with no reply, assume the report was missed and comment on the advisory again. If ninety days pass with no fix and no explanation, treat that as agreement to disclose publicly.

Scope

In scope:

  • the application code in this repository
  • the release pipeline: the GitHub Actions workflow, the packaging scripts, and the pinned dependency set they build from
  • anything that lets a file on a device, a network response, or a crafted music library take control of the app, read outside its own data directory, or write outside the paths it is meant to touch
  • leaking the Last.fm session key or a user's listening history to another local account or off the machine

Out of scope:

  • the Last.fm, LRCLIB, MusicBrainz and Cover Art Archive services themselves
  • vulnerabilities in Qt, Mutagen, rsgain or any other dependency. Report those upstream, and tell us as well if a NokiPod release ships an affected version
  • attacks that need an attacker who is already running code as you on your own machine
  • NokiPod keeping a listening history on disk, and putting a copy of it on the device once you have switched that on. That is what the app is for. The exact shape of that copy, and what an older build already did without asking, are spelled out under Known limitations below, so a report that restates them will be closed as known

Known limitations

These describe the v0.1.1 candidate and later source unless a paragraph says otherwise; public v0.1.0 predates some of the controls. They are written down rather than hidden, and a report showing one is worse than described here is still worth sending.

Copying the merged listening history to the device is off by default, and a copy that does land there carries no permissions. Settings has a Copy play history to iPod toggle. It is off unless you switch it on, and a missing setting counts as off, so nothing about your listening reaches the device until you ask for it. The merged log is built either way, as scrobble-log-export.log in the app's own data folder, where it is written 0600 inside a 0700 directory.

Switch the toggle on and that same merged log is copied to scrobble-log-export.log in the root of the device: when the device is connected and its library scan finishes, again after every batch Last.fm accepts, and again on the Export & sync now button. It also runs with Auto-scrobble on connect switched off and with no Last.fm account linked at all, because the copy happens before either of those is checked. What lands is the whole cumulative history, not just the plays from this session. The write itself is containment checked against the mount and opened O_NOFOLLOW with mode 0600, but an iPod is FAT formatted, and FAT has no owner field and no permission bits, so that mode has nowhere to be stored and is simply dropped. What ls shows for the file comes from the mount's fmask instead, which is usually 0644. The practical consequence is not really about other accounts on your own computer, since the mount directory itself normally keeps them out. It is that the history is now on removable media: plug that iPod into any other machine, or any other operating system, and the file is sitting in the root of it with nothing protecting it. No amount of care on the writing side changes that, which is why the copy is a choice rather than the default.

An older build has probably left one there already. Every version before this one wrote scrobble-log-export.log to the device on every sync, with no setting to stop it, so if you have run one, assume the file is on your iPod now. Switching the toggle off stops NokiPod writing it again; it does not remove a copy already on the disk. Delete it from the iPod, the button next to the toggle in Settings, does that with the device plugged in. Switch the toggle off first, because the two are independent and the next sync would otherwise put the file straight back. Deleting scrobble-log-export.log from the root of the device yourself works just as well.

A hardlink on the device is not contained. Everything NokiPod reads from or writes to a device is checked against the mount first, and that check resolves symlinks. It cannot resolve a hardlink, because a hardlink is not a reference to another path, it is a second name for the same file. A hardlink inside the device pointing at a file outside it therefore passes, and a tag write would modify the host file. Checking the device number does not help: a hardlink cannot cross filesystems, so on a real iPod this is impossible anyway, and the only case where it is possible is a forced mount aimed at a folder on your own disk, where the two share a device number by definition.

A forced mount is trusted as given. NOKIPOD_FORCE_MOUNT names the device root outright, with no check that it is a mount point or that it looks like an iPod. Containment then means "nothing escapes that folder", which is exactly what it says, but the folder itself is whatever you pointed it at. If you point it at your home directory, your home directory is the device.

The mount point itself is not inspected, only what is under it. A symlink at the mount point is accepted, deliberately: on macOS /tmp is a symlink to /private/tmp, and refusing links above the root would refuse whole libraries for a reason that has nothing to do with the device.

All three need you to have set the situation up yourself. The threat this containment is built for is the other one: a device someone else prepared, plugged into a machine that trusts it.

Device-controlled media parsing is bounded, but not process-isolated. An audio extension is only a filename, so every parser still receives a container the device chose. All runtime Mutagen reads and tag writes go through the same guard: one parse may read at most 128 MB from the file, aggregate zlib expansion across compressed ID3 frames is refused above 32 MB per parse, and retained metadata strings are cut to 4096 characters. Deterministic non-audio refusals are remembered until the file changes; over-budget, parser, and transient I/O failures are retried on a later scan rather than hiding a real track forever.

Artwork has additional downstream limits. A picture is refused past 20 MB, past 12000 pixels on a side, or past 40 million pixels in total, and Qt answers the size question from the header before the full decode; downloaded or newly embedded art is held to 10 MB. A tag field written by the editor is refused past 1000 characters. A log the device wrote is refused past 16 MB, past 64 KB on one line, past 4096 header lines, or past 200,000 plays, and it is read in bounded chunks.

Those are byte and expansion ceilings, not a hard CPU or whole-process memory sandbox. Mutagen can parse more than once during one operation; decoded Python objects have overhead; non-ID3 containers and Qt's image/audio decoders have their own bounded-input work; and a hostile file could still make a parser use substantial CPU before it returns. A separate parser process with an OS memory limit and wall-clock kill would provide a stronger boundary. Neither Mutagen nor the Qt media/image decoders has been fuzzed specifically for this project, so that residual parser surface is stated rather than claimed clean.

Verifying a release

Starting with v0.1.1, the workflow refuses to publish unless the release can carry, alongside the Linux AppImage and Windows installer:

  • SHA256SUMS.txt, the checksums of the published files
  • nokipod-<tag>-python-dependencies-sbom.cdx.json, a CycloneDX inventory of the Python packages pinned in requirements.lock.txt. It does not claim to inventory packaging tools, an OS base image, or the AppImage runtime
  • nokipod-<tag>-windows-runtime-sbom.cdx.json, a CycloneDX inventory of the Windows frozen/native runtime: the PyInstaller bootloader; CPython with bzip2/XZ/libffi/mpdecimal/OpenSSL; PyQt/Qt/PyQt6-sip; Qt Multimedia's FFmpeg/zlib; rsgain/FFmpeg/TagLib/libebur128/getopt/zlib/inih/utfcpp; and the exact MSVC runtime DLL versions and per-file binary hashes. The same file, native notices, SOURCE-PROVENANCE.txt, and NATIVE-SOURCE-OFFER.txt are installed in the rsgain folder
  • a signed build provenance attestation over each of those files, recording which workflow run and which commit produced those exact bytes

That third one used to be a softer claim than it read. The workflow recorded provenance after publishing, and it was allowed to fail without failing the release. The v0.1.1 workflow records it before upload and refuses publication when attestation is unavailable. These exact changes have not yet had their first hosted run, so verify any eventual release rather than inferring that the gate ran from this document alone.

An attestation proves provenance: it identifies the workflow and commit that produced a particular set of bytes. It does not prove that an independent rebuild will produce identical bytes. The workflow pins the packaged Python runtime to 3.13.15. It builds rsgain from exact rsgain and vcpkg commits whose archives are SHA-256-verified; the pinned vcpkg recipes select FFmpeg 9.0.1 and hash the transitive native sources. The exact official vcpkg-tool 2026-07-27 asset is independently SHA-256-pinned and must carry a valid Microsoft Authenticode signature before it is first executed. CPython's superseded OpenSSL 3.0.21 DLLs are replaced with source-built OpenSSL 3.0.22 from an exact SHA-256-pinned official archive and checked DLL-name patch. The gate verifies CPython's exact imports/version, a real certificate-checked HTTPS request, Qt's Schannel/cert-only backend set and Schannel HTTPS, the installed DLL hashes, and the absence of competing suffixed OpenSSL DLLs. FFmpeg and the other copyleft native libraries remain replaceable DLLs, and the installer includes their notices, hashes, rebuild path, and corresponding-source offer.

The rsgain payload uses FFmpeg n9.0.1 at annotated tag commit bf1b838f. Although FFmpeg's public security table does not yet have a 9.0 section, Git ancestry puts the published c23d4da, 5806e8b9, and 374b726f fixes for CVE-2026-8461 and 144af8f8 for CVE-2026-30999 before that tag. The release build independently downloads the exact SHA-512-pinned n9.0.1 archive and fails closed unless the source still contains all four security changes; it does not infer safety from the major version number.

The Windows job pins the maintained windows-2022 image to match its Visual Studio 2022 generator, but that image's compiler contents and Ubuntu packages selected from signed repositories can still change. SHA256SUMS.txt verifies the release bytes you downloaded; it is not a reproducible-build claim.

The Windows installer rejects systems below x64 Windows 10 1809 and the native build sets Windows 10/RS5 compile targets, rejects DLL imports outside a literal reviewed Windows/API-set allowlist, and records the actual import closure. Those controls do not emulate the 1809 loader: the finished installer has not yet completed a native Windows 10 1809 VM smoke test, so that exact floor remains an explicit validation boundary rather than an inferred CI result.

After v0.1.1 is published, Linux users can verify the manifest and provenance:

sha256sum -c SHA256SUMS.txt
gh attestation verify NokiPod-0.1.1-x86_64.AppImage --repo Nokikuji/NokiPod

On Windows, run the following in PowerShell from the directory containing the installer and SHA256SUMS.txt, then compare the displayed hash with the NokiPod-Setup-0.1.1.exe entry in SHA256SUMS.txt:

Get-FileHash .\NokiPod-Setup-0.1.1.exe -Algorithm SHA256
gh attestation verify .\NokiPod-Setup-0.1.1.exe --repo Nokikuji/NokiPod

gh attestation verify fails both when no attestation exists and when one exists but does not match the file, which are very different problems. If you want to see whether there is anything on record at all before verifying it:

gh attestation download NokiPod-0.1.1-x86_64.AppImage --repo Nokikuji/NokiPod

A file with nothing on record did not come out of this repository's release workflow, whatever page you found it on.

The AppImage itself is not code signed yet, and neither is the Windows installer. The provenance attestation and platform code signing answer different questions: the attestation ties exact bytes to this repository's GitHub workflow and commit, while code signing would let the operating system identify the publisher at launch. Until signing keys exist, verify the checksum and attestation; Windows SmartScreen may still warn about the unsigned installer.

Updates use a fresh verified download

NokiPod has no update check or updater. Release AppImages contain no embedded update information, and the release publishes no .zsync sidecar. To update, download the complete new AppImage or Windows installer from the releases page and verify its checksum and provenance with the commands above before running it.

There aren't any published security advisories