Skip to content

Commit aca67f6

Browse files
committed
HttpUtility: add functions for validating HTTP header names and values
1 parent 519da9c commit aca67f6

File tree

4 files changed

+152
-0
lines changed

4 files changed

+152
-0
lines changed

lib/remote/httputility.cpp

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,74 @@ void HttpUtility::SendJsonError(HttpResponse& response,
7878

7979
HttpUtility::SendJsonBody(response, params, result);
8080
}
81+
82+
/**
83+
* Check if the given string is suitable to be used as an HTTP header name.
84+
*
85+
* @param name The value to check for validity
86+
* @return true if the argument is a valid header name, false otherwise
87+
*/
88+
bool HttpUtility::IsValidHeaderName(std::string_view name)
89+
{
90+
/*
91+
* Derived from the following syntax definition in RFC9110:
92+
*
93+
* field-name = token
94+
* token = 1*tchar
95+
* tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
96+
* ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
97+
* DIGIT = %x30-39 ; 0-9
98+
*
99+
* References:
100+
* - https://datatracker.ietf.org/doc/html/rfc9110#section-5.1
101+
* - https://datatracker.ietf.org/doc/html/rfc9110#appendix-A
102+
* - https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1
103+
*/
104+
105+
return !name.empty() && std::all_of(name.begin(), name.end(), [](char c) {
106+
switch (c) {
107+
case '!': case '#': case '$': case '%': case '&': case '\'': case '*': case '+':
108+
case '-': case '.': case '^': case '_': case '`': case '|': case '~':
109+
return true;
110+
default:
111+
return ('0' <= c && c <= '9') || ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z');
112+
}
113+
});
114+
}
115+
116+
/**
117+
* Check if the given string is suitable to be used as an HTTP header value.
118+
*
119+
* @param value The value to check for validity
120+
* @return true if the argument is a valid header value, false otherwise
121+
*/
122+
bool HttpUtility::IsValidHeaderValue(std::string_view value)
123+
{
124+
/*
125+
* Derived from the following syntax definition in RFC9110:
126+
*
127+
* field-value = *field-content
128+
* field-content = field-vchar [ 1*( SP / HTAB / field-vchar ) field-vchar ]
129+
* field-vchar = VCHAR / obs-text
130+
* obs-text = %x80-FF
131+
* VCHAR = %x21-7E ; visible (printing) characters
132+
*
133+
* References:
134+
* - https://datatracker.ietf.org/doc/html/rfc9110#section-5.5
135+
* - https://datatracker.ietf.org/doc/html/rfc9110#appendix-A
136+
* - https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1
137+
*/
138+
139+
if (!value.empty()) {
140+
// Must not start or end with space or tab.
141+
for (char c : {*value.begin(), *value.rbegin()}) {
142+
if (c == ' ' || c == '\t') {
143+
return false;
144+
}
145+
}
146+
}
147+
148+
return std::all_of(value.begin(), value.end(), [](char c) {
149+
return c == ' ' || c == '\t' || ('\x21' <= c && c <= '\x7e') || ('\x80' <= c && c <= '\xff');
150+
});
151+
}

lib/remote/httputility.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ class HttpUtility
2626
static void SendJsonBody(HttpResponse& response, const Dictionary::Ptr& params, const Value& val);
2727
static void SendJsonError(HttpResponse& response, const Dictionary::Ptr& params, const int code,
2828
const String& info = {}, const String& diagnosticInformation = {});
29+
30+
static bool IsValidHeaderName(std::string_view name);
31+
static bool IsValidHeaderValue(std::string_view value);
2932
};
3033

3134
}

test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ set(base_test_SOURCES
123123
remote-configpackageutility.cpp
124124
remote-httpserverconnection.cpp
125125
remote-httpmessage.cpp
126+
remote-httputility.cpp
126127
remote-url.cpp
127128
${base_OBJS}
128129
$<TARGET_OBJECTS:config>

test/remote-httputility.cpp

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
2+
3+
#include <BoostTestTargetConfig.h>
4+
#include "remote/httputility.hpp"
5+
#include "test/icingaapplication-fixture.hpp"
6+
7+
using namespace icinga;
8+
9+
BOOST_AUTO_TEST_SUITE(remote_httputility)
10+
11+
BOOST_AUTO_TEST_CASE(IsValidHeaderName)
12+
{
13+
// Use string_view literals (""sv) to allow test inputs containing '\0'.
14+
using namespace std::string_view_literals;
15+
16+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderName("Host"sv), true);
17+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderName("X-Powered-By"sv), true);
18+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderName("Content-Security-Policy"sv), true);
19+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderName("Strict-Transport-Security"sv), true);
20+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderName("lowercase-is-fine-too"sv), true);
21+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderName("everything-from-the-spec-!#$%&'*+-.^_`|~0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"sv), true);
22+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderName("-this-seems-to-be-allowed-too-"sv), true);
23+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderName("~http~is~weird~"sv), true);
24+
25+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderName(""sv /* empty header name is invalid */), false);
26+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderName("spaces are not allowed"sv), false);
27+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderName("tabs\tare\tnot\tallowed"sv), false);
28+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderName("nul-is-bad\0"sv), false);
29+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderName("del-is-bad\x7f"sv), false);
30+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderName("non-ascii-is-bad\x80"sv), false);
31+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderName("non-ascii-is-bad\xff"sv), false);
32+
}
33+
34+
BOOST_AUTO_TEST_CASE(IsValidHeaderValue)
35+
{
36+
// Use string_view literals (""sv) to allow test inputs containing '\0'.
37+
using namespace std::string_view_literals;
38+
39+
auto everything = []{
40+
std::string s = "everything-from-the-spec \t ";
41+
for (int i = 0x21; i <= 0x7e; ++i) {
42+
s.push_back(char(i));
43+
}
44+
for (int i = 0x80; i <= 0xff; ++i) {
45+
s.push_back(char(i));
46+
}
47+
48+
// Sanity checks:
49+
for (char c : {'\x00', '\x08', '\x0a', '\x1f', '\x7f'}) {
50+
BOOST_CHECK_EQUAL(s.find(c), std::string::npos);
51+
}
52+
for (char c : {'\t' /* == 0x09 */, ' ' /* == 0x20 */, '\x21', '\x7e', '\x80', '\xff'}) {
53+
BOOST_CHECK_NE(s.find(c), std::string::npos);
54+
}
55+
56+
return s;
57+
};
58+
59+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderValue(""sv /* empty header value is allowed */), true);
60+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderValue("example.com"sv), true);
61+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderValue("default-src 'self'; img-src 'self' example.com"sv), true);
62+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderValue("max-age=31536000"sv), true);
63+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderValue("spaces are allowed"sv), true);
64+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderValue("tabs\tare\tallowed"sv), true);
65+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderValue("non-ascii-is-allowed\x80"sv), true);
66+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderValue("non-ascii-is-allowed\xff"sv), true);
67+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderValue(everything()), true);
68+
69+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderValue("nul-is-bad\0"sv), false);
70+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderValue("del-is-bad\x7f"sv), false);
71+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderValue(" no leading spaces"sv), false);
72+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderValue("no trailing spaces "sv), false);
73+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderValue("\tno leading tabs"sv), false);
74+
BOOST_CHECK_EQUAL(HttpUtility::IsValidHeaderValue("no trailing tabs\t"sv), false);
75+
}
76+
77+
BOOST_AUTO_TEST_SUITE_END()

0 commit comments

Comments
 (0)