Field names are case-insensitive (RFC 9110 5.1). Go's net/http canonicalises them before writing, so chlink's "X-CP-Token" reaches us as "X-Cp-Token": a case-sensitive lookup finds no PIN and every chlink upload dies with 403. Matching is also anchored to the start of a header line, so a value that happens to contain " :" cannot impersonate the header.
| 183 | // Matching is also anchored to the start of a header line, so a value that |
| 184 | // happens to contain "<key>:" cannot impersonate the header. |
| 185 | std::string headerValue(const std::string& headers, const std::string& key) |
| 186 | { |
| 187 | size_t lineStart = 0; |
| 188 | while (lineStart < headers.size()) { |
| 189 | size_t lineEnd = headers.find("\r\n", lineStart); |
| 190 | if (lineEnd == std::string::npos) { |
| 191 | lineEnd = headers.size(); |
| 192 | } |
| 193 | |
| 194 | size_t colon = headers.find(':', lineStart); |
| 195 | if (colon != std::string::npos && colon < lineEnd && colon - lineStart == key.size()) { |
| 196 | bool match = true; |
| 197 | for (size_t i = 0; i < key.size(); ++i) { |
| 198 | if (tolower((unsigned char)headers[lineStart + i]) != tolower((unsigned char)key[i])) { |
| 199 | match = false; |
| 200 | break; |
| 201 | } |
| 202 | } |
| 203 | if (match) { |
| 204 | size_t valueStart = colon + 1; |
| 205 | while (valueStart < lineEnd && (headers[valueStart] == ' ' || headers[valueStart] == '\t')) { |
| 206 | valueStart++; |
| 207 | } |
| 208 | size_t valueEnd = lineEnd; |
| 209 | while (valueEnd > valueStart && (headers[valueEnd - 1] == ' ' || headers[valueEnd - 1] == '\t')) { |
| 210 | valueEnd--; |
| 211 | } |
| 212 | return headers.substr(valueStart, valueEnd - valueStart); |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | if (lineEnd == headers.size()) { |
| 217 | break; |
| 218 | } |
| 219 | lineStart = lineEnd + 2; |
| 220 | } |
| 221 | return ""; |
| 222 | } |
| 223 | |
| 224 | bool constantTimeEquals(const std::string& a, const std::string& b) |
| 225 | { |
no test coverage detected