AddHeader writes its arguments verbatim into the response stream, so any caller that passes attacker-controlled bytes can split headers / inject a response.
AddHeader("Location", m_sURIPrefix + sURL) where sURL.StartsWith("/") is required and every caller passes a controlled path.
So this is not currently exploitable. But the function is exported via CHTTPSock and any module author can call it with user-supplied data. A two-line guard saves the next contributor from a footgun.
(Could also assert rather than silently ignore; either works. Filtering seems safer than asserting given that production builds don't run with asserts on.)
Filed as a hardening note, no PoC: there is no caller today that hits this.
`AddHeader` writes its arguments verbatim into the response stream, so any caller that passes attacker-controlled bytes can split headers / inject a response.
```cpp
// src/HTTPSock.cpp
765: void CHTTPSock::AddHeader(const CString& sName, const CString& sValue) {
766: m_msHeaders[sName] = sValue;
767: }
...
749: for (const auto& it : m_msHeaders) {
750: Write(it.first + ": " + it.second + "\r\n");
751: }
```
I traced every call site in `src/` and `modules/`. None currently passes attacker-controlled bytes:
- `AddHeader("Content-Encoding", "gzip")`: literal
- `AddHeader("Last-Modified", GetDate(iMTime))`: RFC1123 date, no CRLF
- `AddHeader("ETag", "\"" + sETag + "\"")` with `sETag = "-" + iMTime`
- `AddHeader("Cache-Control", "public")`: literal
- `AddHeader("WWW-Authenticate", "Basic realm=\"ZNC\"")`: literal
- `AddHeader("Location", m_sURIPrefix + sURL)` where `sURL.StartsWith("/")` is required and every caller passes a controlled path.
So this is not currently exploitable. But the function is exported via `CHTTPSock` and any module author can call it with user-supplied data. A two-line guard saves the next contributor from a footgun.
## Fix
```cpp
void CHTTPSock::AddHeader(const CString& sName, const CString& sValue) {
if (sName.find_first_of("\r\n") != CString::npos) return;
if (sValue.find_first_of("\r\n") != CString::npos) return;
m_msHeaders[sName] = sValue;
}
```
(Could also `assert` rather than silently ignore; either works. Filtering seems safer than asserting given that production builds don't run with asserts on.)
Filed as a hardening note, no PoC: there is no caller today that hits this.
<!-- github-sync-id:4326795815 -->
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
AddHeaderwrites its arguments verbatim into the response stream, so any caller that passes attacker-controlled bytes can split headers / inject a response.I traced every call site in
src/andmodules/. None currently passes attacker-controlled bytes:AddHeader("Content-Encoding", "gzip"): literalAddHeader("Last-Modified", GetDate(iMTime)): RFC1123 date, no CRLFAddHeader("ETag", "\"" + sETag + "\"")withsETag = "-" + iMTimeAddHeader("Cache-Control", "public"): literalAddHeader("WWW-Authenticate", "Basic realm=\"ZNC\""): literalAddHeader("Location", m_sURIPrefix + sURL)wheresURL.StartsWith("/")is required and every caller passes a controlled path.So this is not currently exploitable. But the function is exported via
CHTTPSockand any module author can call it with user-supplied data. A two-line guard saves the next contributor from a footgun.Fix
(Could also
assertrather than silently ignore; either works. Filtering seems safer than asserting given that production builds don't run with asserts on.)Filed as a hardening note, no PoC: there is no caller today that hits this.