CSP, CSRF, Cookies
ChannelWatch adds a set of security headers to every response and applies a few different request-protection patterns depending on how the request is authenticated. The details below are taken from the current backend middleware in source/ui/backend/main.py.
Content Security Policy
Section titled “Content Security Policy”ChannelWatch sets a Content-Security-Policy header on every response. The current policy is:
default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' https: data: blob:; font-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'In practice, that means:
- Scripts and styles are loaded only from the same origin (
'self'). - Inline scripts and inline styles are not allowed by the policy.
- Images can load from the same origin, HTTPS URLs,
data:URLs, andblob:URLs. - Fonts can load from the same origin and
data:URLs. frame-ancestors 'self'allows same-origin framing only. It does not setframe-ancestors 'none'.
The middleware also sets X-Frame-Options: SAMEORIGIN, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, and Permissions-Policy: camera=(), microphone=(), geolocation=().
If you run ChannelWatch behind a reverse proxy, make sure the proxy does not strip or replace these headers unless you are intentionally tightening them.
CSRF protection
Section titled “CSRF protection”The current backend uses two different CSRF defenses.
Authenticated API routes
Section titled “Authenticated API routes”For the current shared-key flow, the main CSRF defense is the custom X-API-Key header. The middleware comments call this out explicitly. Browsers cannot add that header to a cross-origin fetch without a CORS preflight, and the backend sets allow_origins=[] with allow_credentials=False, so cross-origin preflights are rejected.
RBAC browser sessions
Section titled “RBAC browser sessions”When RBAC session auth is in use, mutating API requests (POST, PUT, PATCH, DELETE) also require an X-CSRF-Token header that matches the server-side session token. The current tests cover this flow directly: login returns both the channelwatch_session cookie and a CSRF token, write requests without a matching token fail with 403, and matching tokens allow the write when the user’s role is sufficient.
When CW_DISABLE_AUTH=true
Section titled “When CW_DISABLE_AUTH=true”When auth is disabled, the backend still adds an origin check for state-changing API requests. If a request includes an Origin header and the origin host does not match the request Host, the backend rejects it with 403 Cross-site request rejected.
Public feeds and tokens
Section titled “Public feeds and tokens”The ICS, RSS, and Atom feed routes are intentionally auth-exempt in middleware. They do not use X-API-Key or an RBAC session cookie.
Instead, protection for those routes comes from the per-feed token in the URL when the feed is enabled. If you turn on public feeds and share the tokenized URL, treat that URL as a credential. If you enable a feed without keeping its token private, the security badge can correctly show the higher-exposure API_KEY_ONLY state.
Secure cookie flags
Section titled “Secure cookie flags”RBAC login sets the channelwatch_session cookie with the following flags in source:
| Flag | Value | Effect |
|---|---|---|
HttpOnly | Always | JavaScript cannot read the session cookie |
SameSite | Strict | Cookie is not sent on cross-site requests |
Secure | Always in the current backend | Cookie is marked for HTTPS transport only |
The login route sets those flags directly, and the response middleware also appends HttpOnly, Secure, and SameSite=Strict to any Set-Cookie header that is missing them.
GET /api/v1/auth/whoami reads that cookie-backed session, and POST /api/v1/auth/logout invalidates the stored session token before clearing the cookie.
Threat model
Section titled “Threat model”The following is the current source-backed threat model. It is meant to show what the shipping middleware protects, and where the protection stops.
What the current API-key and CSRF design protects against: A malicious site cannot silently issue authenticated cross-origin API requests with your X-API-Key header. Browsers need a preflight to send that custom header, and the backend rejects cross-origin preflights by default.
What the current API-key and CSRF design does not protect against: Anyone who already knows the shared API key can make the same authenticated requests you can. Treat the X-API-Key value as a full-access secret.
What CSP protects against: CSP reduces the browser’s ability to execute injected scripts, load unexpected third-party assets, or submit forms to unexpected origins.
What CSP does not protect against: CSP does not protect your config volume, your backups, or a browser or device that is already compromised.
What public-feed tokens protect against: They let you expose calendar or activity feeds without also exposing the shared API key.
What public-feed tokens do not protect against: Anyone who gets the feed URL can read that feed. Feed tokens are bearer-style secrets in the URL.
Reporting vulnerabilities
Section titled “Reporting vulnerabilities”ChannelWatch uses GitHub’s private security advisory system for vulnerability reports. This keeps the details confidential until a fix is ready.
To report a vulnerability:
- Go to the ChannelWatch GitHub repository.
- Click Security in the top navigation.
- Click Report a vulnerability.
- Fill in the advisory form with a description, reproduction steps, and potential impact.
Response SLA:
- Acknowledgment within 48 hours of receipt.
- Initial assessment and response within 5 business days.
- Fix timeline communicated after assessment.
Please do not open a public issue for security vulnerabilities. Public disclosure before a fix is available puts all users at risk.
Once a fix is released, the vulnerability will be disclosed in the GitHub security advisory and in the release notes for the affected version.
Reverse proxy recommendations
Section titled “Reverse proxy recommendations”If you expose ChannelWatch outside your local network, run it behind a reverse proxy with TLS. A minimal Nginx configuration:
server { listen 443 ssl; server_name channelwatch.example.com;
ssl_certificate /etc/ssl/certs/channelwatch.crt; ssl_certificate_key /etc/ssl/private/channelwatch.key;
location / { proxy_pass http://127.0.0.1:8501; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Proto https; }}X-Forwarded-Proto: https is still a sensible proxy header to pass upstream, but the current backend does not rely on it to decide whether to mark cookies Secure. TLS still matters, because browsers will only send a Secure cookie over HTTPS.
Related pages
Section titled “Related pages”- Security Modes — the three authentication modes and the security badge
- API Keys (encrypted) — how DVR credentials are stored and rotated
- Optional RBAC — user roles and the admin bootstrap flow