Please do not open a public issue for a security problem. Use GitHub's private vulnerability reporting instead:
https://github.com/kfox/c64cast/security/advisories/new
That opens a private advisory visible only to the maintainer. If GitHub reporting is unavailable to you, mail c64cast@gmail.com instead.
Include what you
were running (c64cast --doctor output is ideal), which network surface is
involved, and what an attacker gains. If you have a proof of concept, attach it
there rather than posting it publicly.
Expect an acknowledgment within a week. c64cast is a hobby project maintained by one person, so fixes ship on a best-effort schedule; you will be told which release carries the fix, and credited in the advisory and the changelog unless you would rather not be.
Only the latest released version is supported. While c64cast is 0.x, fixes go
out in a new release rather than as patches to older ones.
c64cast is a LAN tool for hardware sitting on your desk. Several of its features open network listeners, and all but one of them authenticate no callers — that is by design, not an oversight, and it is why none of them belong on an internet-facing interface. Do not port-forward them, and do not run them on an untrusted network. Two of them can be locked: the HTTP control plane can be put behind a shared token, and the web console host is always behind one (below). That is a lock on the door, not a reason to expose the port.
| Surface | Default | Exposure |
|---|---|---|
HTTP control plane ([control]) |
off; binds 127.0.0.1:8765; no token |
Unauthenticated by default: any POST /pause, /skip, /reload from anything that can reach the port controls the run. Localhost-only unless you change host. Setting [control].token (or $C64CAST_CONTROL_TOKEN) requires that token on every route, including the console and its WebSocket; viewer_token grants reads only. |
Web console host (--serve / [web]) |
off; binds 127.0.0.1:8123; always token-gated |
The only surface that starts and stops hardware on request, so it has no unauthenticated mode: with no token configured, one is generated, stored 0600 under the data directory, and printed at startup. The control-plane routes and the performance console ride the same port and the same token. The browser console it serves is behind the same gate — no page, script or stylesheet of it is public, and an unauthenticated navigation gets a form asking for the token rather than the application. viewer_token grants reads only. A full token is remote control of the machine — the sessions it starts open whatever media paths and URLs the configuration names. |
Web console config browser ([web].config_roots) |
the directory the host was launched from | The only surface that reads and writes files on the host. Confined to the configured roots (resolved, so a symbolic link out of one is refused) and to .toml names, and a write must load before it lands. A full token is required — viewer_token cannot write. See the note below on what that access is actually worth. |
| Phone/web performance console | off; shares the control-plane server | Rides the same port, so reaching it from a phone means binding the control plane to a LAN address — which exposes the control plane too, under the same token or the same absence of one. |
WLED bridge Mode 1 ([wled].listen) |
off; binds 0.0.0.0:8080 when enabled |
Presents a virtual WLED device on the LAN, deliberately reachable so the WLED app and Home Assistant can discover it via mDNS. The WLED JSON API has no authentication concept, so anything on the LAN can change scenes and live parameters. |
WLED audio-sync broadcast ([wled] Mode 3) |
off | Plaintext UDP to multicast 239.0.0.1:11988. Carries audio-feature data, not audio. |
WLED pixel sink (wled scene) |
off | Accepts an unauthenticated realtime pixel stream (DDP / WLED UDP) from LedFx or xLights. |
| Ultimate 64 / TeensyROM+ link | required | Outbound only. Writes go over the Ultimate DMA Service (TCP 64) and REST; the C64 side has no meaningful access control, so anything that can reach your U64 can already drive it with or without c64cast. |
The control-plane token is a shared secret sent over plain HTTP, so it is
readable by anything that can watch the traffic on your network. It stops a
housemate's laptop and a curious phone, not an attacker on the wire. It is
supplied through $C64CAST_CONTROL_TOKEN / $C64CAST_CONTROL_VIEWER_TOKEN or
the [control].token / viewer_token config keys, and has no CLI flag for
the same reason the DMA password doesn't. c64cast warns when the control plane
binds a non-loopback address with no token, but does not refuse — the surface
predates the token, and breaking those runs is not the token's business.
The web console's token works the same way and is supplied the same way
($C64CAST_WEB_TOKEN / $C64CAST_WEB_VIEWER_TOKEN, [web].token /
token_file / viewer_token, no CLI flag). The difference is that it has no
"off": that surface has no history to preserve and it owns the hardware, so a
host with no token configured mints one rather than binding open.
Treat a full web-console token as shell-equivalent on that host. The root
list bounds which files the browser may edit, not what a saved file can then
reach: a configuration names media paths and URLs that a session will open and
that yt-dlp will fetch. Confining the editor is worth doing — it is why
config_roots defaults to one directory rather than the whole filesystem — but
it is a blast-radius limit on the editing, not a sandbox around the run.
The Ultimate's optional DMA password is supplied through the
C64CAST_DMA_PASSWORD environment variable or the [ultimate64].dma_password
config key, and deliberately has no CLI flag so it cannot land in shell
history or in ps output. --save-settings and the config serializer refuse to
write it to disk. Treat it as a weak gate against accidents rather than a
security boundary.
c64cast decodes media you point it at (video, images, .sid files) with
OpenCV, PyAV/FFmpeg, and NumPy, and fetches remote content for the RSS, weather,
and URL-playback features. A malicious file or feed is handled by those
libraries' parsers, not by c64cast's own code — keep the optional dependencies
current, yt-dlp especially. That is why yt-dlp is the one dependency with no
version ceiling: it is network-facing, and running a recent release is part of
its security posture.
The launcher scene hands the machine over to a native .prg/.crt, which
then runs unrestricted on the C64. That is 6502 code on real hardware, outside
anything c64cast can sandbox. Run programs you trust.