Please report security issues privately: open a GitHub security advisory on the repo. Do not file a public issue.
Capsaicin sits on three attack surfaces:
- Malicious server. A user runs
capsaicin connectorcapsaicin viewagainst a server they don't control. The server can send arbitrary bytes; the client has only the public RSA key and the password. - Malicious client. Anyone reachable on the listening port of
capsaicin servecan send arbitrary bytes. They need the password to get past auth, but the link-handshake bytes are processed before auth completes. - Adversarial peer in established session. Post-auth, the peer can still send arbitrary protocol bytes, malformed messages, malicious image payloads, etc.
Capsaicin currently runs over plain TCP — anything in transit is visible to passive observers and modifiable by active ones. Until TLS lands, treat the wire as fully untrusted by network observers and only deploy on trusted networks.
The first round of OWASP / red-team review (April 2026) drove these:
- Pre-auth allocation cap.
LinkHeader::sizeis capped atMAX_LINK_PAYLOAD = 4 KiBbefore anyVecis allocated, on both client and server. Was: pre-auth 4 GiB OOM via a single TCP packet. - Image dimension cap. Every codec entry point (
capsaicin-quic,capsaicin-lz,capsaicin-glz) validateswidth × height × bppwithchecked_mulagainstMAX_IMAGE_BYTES = 64 MiBandMAX_IMAGE_DIM = 16384per axis before allocating output buffers. Was: 16 GiB allocations / silent u32-wrap on 32-bit / out-of-bounds writes. - GLZ placeholder cap. When a GLZ image fails to decode and we insert a zero-pixel placeholder so the cross-image dictionary chain stays alive, the placeholder is bounds-checked the same way.
ChannelsList,read_chunksallocation caps. Hostile peer-supplied counts no longer trigger 4 GiBVec::with_capacitycalls.- MJPEG decoder memory cap.
jpeg_decoder::Decoder::set_max_decoding_buffer_size(64 MiB)plus an explicit 16384 dimension cap before allocating the BGRA expansion buffer. - Viewer framebuffer wrap. The viewer no longer panics if a server sends a
SurfaceCreatedwhosewidth × heightoverflowsu32— dimensions are validated and the surface request is dropped with a warning. LinkHeadermajor-version validation. Major-version mismatches now reject the handshake; previously any major version was accepted.- Random session IDs.
Server::new_session_idreturns cryptographically randomu32values fromOsRng. Was: sequentialAtomicU32::fetch_add(1)starting at 1, trivially guessable. - Session table.
Servermaintains aHashSet<u32>of live sessions. Sub-channel attaches whose claimedconnection_idis not in the set are refused. Was: any peer with the password could attach sub-channels to another user's session. - Sub-channel attach refuses unknown sessions (CLI). The reference
servesubcommand callsis_live_sessionbefore honouringaccepted.connection_idfor non-Main attaches. - Connection concurrency cap. The CLI's
serveaccepts at mostMAX_CONCURRENT_CONNS = 64simultaneous connections, gated by atokio::sync::Semaphore. Excess connections are dropped with a warning. --passwordsemantics.servenow refuses to start without an explicit--password VALUE(use--password ''to opt in to no-auth, which logs a warning).--passwordwithout a value is now an error rather than silently empty.- Auth-failure logging. Failed RSA decrypts / wrong-password attempts log
auth: password rejectedat warn level for log-based intrusion detection. Server::password()accessor removed. No reason for callers to read it back; reduces accidental leakage to logs.
These are tracked in the issue tracker / JOURNEY.md and are deliberately not fixed yet:
- No TLS. Plain-TCP only. The wire is unauthenticated and unencrypted. The auth ticket is RSA-OAEP (so the password itself is protected against passive observers), but everything else — keystrokes, framebuffer, future clipboard — is in the clear. Do not run on untrusted networks until TLS lands. (
tokio-rustlsintegration is the next planned hardening.) rsacrate Marvin Attack advisory (RUSTSEC-2023-0071). The crate has a known timing side-channel in PKCS#1 v1.5 decryption. We use OAEP not v1.5, but the crate's CRT path is not fully constant-time. No fixed release exists at time of writing; we'll migrate when 0.10 stabilises.- RSA-1024 keypair, regenerated per process. The 1024-bit key size is mandated by the SPICE protocol's 162-byte SPKI slot; below the modern (NIST SP 800-131A) floor. Mitigated only by being short-lived (per-process) and by TLS pinning when TLS lands.
- Variable-time password compare.
password == expected.as_bytes()is plain==. The ~10–30 ms RSA decryption that precedes it dominates any timing leak from the byte compare, but a constant-time compare viasubtle::ConstantTimeEqis on the to-do list. PROTOCOL_AUTH_SELECTIONcap not implemented. The protocol's optional auth-mechanism-selection step is skipped (we use the legacy fall-through). Acceptable today since AUTH_SPICE is the only mechanism we support, but worth implementing alongside SASL if/when that lands.- No MAC / integrity check on individual messages. The protocol has no per-message integrity, so an active in-path attacker can alter wire bytes — only TLS will protect against this.
- No connection IP throttling. The concurrency cap is global, not per-IP. A single attacker can saturate the cap.
- Encoded
SpiceAddressresolution not implemented. Some SPICE messages reference image data via packedsurface_id:offsetu64 addresses that need a surface cache to resolve. We currently treat all addresses as simple offsets and bail on encoded ones — losing pixel updates but not vulnerable.
- The viewer pulls in
winit,softbuffer,jpeg-decoder, and through them a graph of GUI / X11 / Wayland / macOS / Win32 dependencies. We rely on the upstream maintainers for vulnerabilities in those crates and oncargo auditto surface anything actionable. CI integration ofcargo auditis on the to-do list. - Codecs intentionally don't implement the full SPICE draw command set yet; unsupported commands are dropped as
UnhandledDraw. This is a correctness gap (visible artifacts) rather than a security one.
| Date | Type | Findings |
|---|---|---|
| 2026-04-13 | OWASP Top 10 2021 | 3 HIGH, 5 MEDIUM, multiple LOW |
| 2026-04-13 | Adversarial / red-team | 3 CRITICAL, 4 HIGH, 4 MEDIUM |
All findings from the April 2026 reviews that were classified CRITICAL or HIGH have been addressed in the same commit that created this file. MEDIUM findings are addressed where straightforward; the rest are tracked above under "Known gaps."