HTTPSock: emit standard hardening response headers

Close #2012.

Add X-Frame-Options: SAMEORIGIN, X-Content-Type-Options: nosniff and
Referrer-Policy: same-origin to every response so webadmin and module
pages are framed/sniff-protected by default. Add no-store Cache-Control
and Pragma: no-cache on dynamic responses so shared workstations can't
replay authenticated pages from browser history. Skip the cache headers
for 304 and for static asset MIME types (image, font, text/css,
application/javascript) that the existing ETag/Last-Modified path on
PrintFile already handles.

Per review feedback: the emitter is a private WriteHardeningHeaders that
writes each line via the socket directly from PrintHeader, not a public
helper returning a temporary VCString. Callers can override a default
value with AddHeader, or suppress one outright with the new public
OmitHardeningHeader(name).

Tests: drive PrintHeader on a CHTTPSock subclass that captures Write()
calls, then assert with gmock matchers (Contains(StartsWith(...))).
This commit is contained in:
MarkLee131
2026-04-29 20:29:47 +08:00
parent 866303eef0
commit f71e021e3b
3 changed files with 162 additions and 0 deletions
+34
View File
@@ -740,6 +740,8 @@ bool CHTTPSock::PrintHeader(off_t uContentLength, const CString& sContentType,
}
Write("Content-Type: " + m_sContentType + "\r\n");
WriteHardeningHeaders(uStatusId);
for (const auto& it : m_msResponseCookies) {
Write("Set-Cookie: " + it.first.Escape_n(CString::EURL) + "=" +
it.second.Escape_n(CString::EURL) + "; HttpOnly; path=/;" +
@@ -776,6 +778,38 @@ void CHTTPSock::AddHeader(const CString& sName, const CString& sValue) {
m_msHeaders[sName] = sValue;
}
void CHTTPSock::OmitHardeningHeader(const CString& sName) {
m_ssOmitHardening.insert(sName);
}
void CHTTPSock::WriteHardeningHeaders(unsigned int uStatusId) {
auto writeIfWanted = [&](const CString& sName, const CString& sValue) {
if (m_msHeaders.find(sName) != m_msHeaders.end()) return;
if (m_ssOmitHardening.find(sName) != m_ssOmitHardening.end()) return;
Write(sName + ": " + sValue + "\r\n");
};
// Always-on defaults: callers can override via AddHeader, or skip
// entirely via OmitHardeningHeader, before PrintHeader runs.
writeIfWanted("X-Frame-Options", "SAMEORIGIN");
writeIfWanted("X-Content-Type-Options", "nosniff");
writeIfWanted("Referrer-Policy", "same-origin");
// Don't cache authenticated/dynamic responses. Skip for 304 and for
// static asset MIME types that the ETag/Last-Modified path handles
// explicitly via PrintFile.
const bool bStaticLike =
uStatusId == 304 || m_sContentType.StartsWith("image/") ||
m_sContentType.StartsWith("font/") ||
m_sContentType.StartsWith("text/css") ||
m_sContentType.StartsWith("application/javascript");
if (!bStaticLike) {
writeIfWanted("Cache-Control",
"no-store, no-cache, must-revalidate, max-age=0");
writeIfWanted("Pragma", "no-cache");
}
}
bool CHTTPSock::Redirect(const CString& sURL) {
if (SentHeader()) {
DEBUG("Redirect() - Header was already sent");