open
https://gitlab.synchro.net/main/sbbs/-/work_items/1254
## Summary
`putmsg()` passes SyncTERM's `C;S` (store file) APC through from message
text. A message containing `ESC _SyncTERM:C;S;<name>;<base64> ESC \` writes a file into the SyncTERM cache of everyone who reads it. That cache is a directory on the reader's own disk, kept per BBS across sessions, so the file stays there and is found by anything that later looks up that name.
At least one shipped consumer trusts a cached file by name alone: `src/doors/termgfx/audio_mgr.c` plays door music straight from the client's cache, with no upload, whenever the name is listed. A post that plants a file under the name of a door's music track replaces that track for the reader in later sessions.
## What putmsg() filters
`putmsgfrag()` (`src/sbbs3/putmsg.cpp`) runs each escape sequence through `ANSI_Parser` and strips a fixed list before passing the rest to the terminal: SKR, XTSRGA, DSR, DA, DECTID, DECRQUPSS, DECRQPKFM, DECRQM, DECRQTSR, DECRQPSR, DECRQCRA, DECRQSS, and the SyncTERM `C;L` and `Q;JXL` queries.
Every item on that list is a *query*: something that makes the terminal send
a reply, which would otherwise arrive as the reader's own input. Sequences that change the terminal's state are not filtered, and `C;S` is one of those.
The filtering is gated only on `term->supports(ANSI)`. It makes no distinction between a sysop's display file and a message body, and no mode of `putmsg()` suppresses escape sequences in message text.
## Why the cache matters
SyncTERM keeps `C;S` files in `get_cache_fn_base()`'s directory (`src/syncterm/term.c`): `<cache path>/<dialing entry name>/`, created on demand and never removed. Consumers use `APC SyncTERM:C;L` to find what a client already holds and skip re-uploading it. Whether they are exposed depends on how they decide a listed file is the right one:
| Consumer | Check | Planted file |
|---|---|---|
| `exec/load/syncterm_cache.js` | listed MD5 equals the local file's | re-uploaded over it |
| `xtrn/zmachine/zmachine.js`, `v6isCached()` | listed MD5 equals the local file's | re-uploaded over it |
| `src/doors/termgfx/audio_mgr.c`, `cl_has()` | name only | **played** |
`audio_mgr.c`'s comment says it "mirrors the zmachine's v6cacheList (name-presence, not MD5, since our names are content-addressed)". The zmachine does compare the MD5, and content-addressing protects only against a file changing on the BBS side. It says nothing about who wrote the file on the client.
Checking the MD5 makes a planted file ineffective in later sessions. It does not help a consumer that lists the cache once and trusts that list for the rest of the session, if the reader opens the post after the list was taken.
## Related, session-scoped
The same pass-through lets message text make the reader's terminal play synthesized tones or cached sounds (`A;Synth`, `A;Load`, `A;Queue`), switch fonts (`C;SetFont`), and change the emulated output speed (`CSI Ps ; Ps * r`). These last only for the session, so they are nuisances rather than persistent changes, but they come from the same missing distinction.
## Suggested direction
1. Let `putmsg()` know when it is displaying untrusted text, for example with a
`P_` mode flag set where message bodies are shown, and in that mode strip
SyncTERM APCs (`ESC _SyncTERM:`), at minimum `C;S`. Display files keep full
pass-through, since sysops embed fonts and images in them.
2. Have `audio_mgr.c` compare the listed MD5, as `syncterm_cache.js` and the
zmachine already do, and correct its comment.
## Evidence
This is from reading the code. No message containing `C;S` has been posted to demonstrate it end to end. Not audited: whether any message importer (QWK REP, FidoNet echomail, SMTP) or message editor removes escape sequences before a message is stored. If one does, that path is not exposed, but display does not depend on it.
-- *Authored by Claude (Claude Code), on behalf of @rswindell*
--- SBBSecho 3.37-Linux
* Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)