HTTPSock: emit standard hardening response headers (XFO, nosniff, Cache-Control) #1090

Closed
opened 2026-10-05 05:02:47 +02:00 by adam · 2 comments
Owner

CHTTPSock::PrintHeader: emit standard hardening response headers

PrintHeader doesn't set Cache-Control, X-Frame-Options,
X-Content-Type-Options, or Referrer-Policy for any response. The static
file path adds Cache-Control: public (which is also wrong direction for
authenticated content) but everything else goes out bare.

// src/HTTPSock.cpp
716: bool CHTTPSock::PrintHeader(off_t uContentLength, const CString& sContentType,
                                 unsigned int uStatusId, const CString& sStatusMsg) {
...
734:     Write("HTTP/" + ... + "\r\n");
735:     Write("Date: " + GetDate() + "\r\n");
736:     Write("Server: " + CZNC::GetTag(false) + "\r\n");
737:     if (uContentLength > 0) Write("Content-Length: ...");
741:     Write("Content-Type: " + m_sContentType + "\r\n");
742:     for (Set-Cookie: ...) // SameSite=Strict; HttpOnly; Secure-when-SSL
749:     for (auto& it : m_msHeaders) ...
753:     Write("Connection: Close\r\n");
755:     Write("\r\n");

Concrete consequences:

  • No X-Frame-Options / frame-ancestors → webadmin pages framable. CSRF
    is enforced server-side (WebModules.cpp:661-669), but the admin's
    browser still carries the cookie + token, so clickjacking that triggers
    a state-changing form submit will pass the token check.
  • No X-Content-Type-Options: nosniff → endpoints that serve text/plain
    (samplewebapi, any module that does WebSock.Write(text) with
    Content-Type: text/plain) are MIME-sniffed by old browsers and can
    yield reflected XSS.
  • No Cache-Control on authenticated HTML → previously rendered admin
    pages can be reread from browser history on a shared workstation.

Fix

In PrintHeader, before the trailing \r\n:

Write("X-Frame-Options: SAMEORIGIN\r\n");
Write("X-Content-Type-Options: nosniff\r\n");
Write("Referrer-Policy: same-origin\r\n");
if (uStatusId != 304 && !m_sContentType.StartsWith("image/") &&
    !m_sContentType.StartsWith("font/") && !m_sContentType.StartsWith("text/css")) {
    Write("Cache-Control: no-store, no-cache, must-revalidate, max-age=0\r\n");
    Write("Pragma: no-cache\r\n");
}

Also at line 378, change Cache-Control: public for static files to
Cache-Control: private, max-age=... so shared HTTP caches don't fan out
skin/avatar resources across users.

Happy to PR. Easy to test with curl -i.

# `CHTTPSock::PrintHeader`: emit standard hardening response headers `PrintHeader` doesn't set `Cache-Control`, `X-Frame-Options`, `X-Content-Type-Options`, or `Referrer-Policy` for any response. The static file path adds `Cache-Control: public` (which is also wrong direction for authenticated content) but everything else goes out bare. ```cpp // src/HTTPSock.cpp 716: bool CHTTPSock::PrintHeader(off_t uContentLength, const CString& sContentType, unsigned int uStatusId, const CString& sStatusMsg) { ... 734: Write("HTTP/" + ... + "\r\n"); 735: Write("Date: " + GetDate() + "\r\n"); 736: Write("Server: " + CZNC::GetTag(false) + "\r\n"); 737: if (uContentLength > 0) Write("Content-Length: ..."); 741: Write("Content-Type: " + m_sContentType + "\r\n"); 742: for (Set-Cookie: ...) // SameSite=Strict; HttpOnly; Secure-when-SSL 749: for (auto& it : m_msHeaders) ... 753: Write("Connection: Close\r\n"); 755: Write("\r\n"); ``` Concrete consequences: - No `X-Frame-Options` / `frame-ancestors` → webadmin pages framable. CSRF is enforced server-side (`WebModules.cpp:661-669`), but the admin's browser still carries the cookie + token, so clickjacking that triggers a state-changing form submit will pass the token check. - No `X-Content-Type-Options: nosniff` → endpoints that serve `text/plain` (`samplewebapi`, any module that does `WebSock.Write(text)` with `Content-Type: text/plain`) are MIME-sniffed by old browsers and can yield reflected XSS. - No `Cache-Control` on authenticated HTML → previously rendered admin pages can be reread from browser history on a shared workstation. ## Fix In `PrintHeader`, before the trailing `\r\n`: ```cpp Write("X-Frame-Options: SAMEORIGIN\r\n"); Write("X-Content-Type-Options: nosniff\r\n"); Write("Referrer-Policy: same-origin\r\n"); if (uStatusId != 304 && !m_sContentType.StartsWith("image/") && !m_sContentType.StartsWith("font/") && !m_sContentType.StartsWith("text/css")) { Write("Cache-Control: no-store, no-cache, must-revalidate, max-age=0\r\n"); Write("Pragma: no-cache\r\n"); } ``` Also at line 378, change `Cache-Control: public` for static files to `Cache-Control: private, max-age=...` so shared HTTP caches don't fan out skin/avatar resources across users. Happy to PR. Easy to test with `curl -i`. <!-- github-sync-id:4326794263 -->
adam closed this issue 2026-10-05 05:02:47 +02:00
Author
Owner

#2012 ?

#2012 ? <!-- github-comment-id:4318480705 -->
Author
Owner

Sorry, this was an accidental duplicate submission on my side. #2012 has the canonical content, please track that one and feel free to close this.

Sorry, this was an accidental duplicate submission on my side. #2012 has the canonical content, please track that one and feel free to close this. <!-- github-comment-id:4318627546 -->
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: adam/znc#1090