Skip to content

Commit aaa4055

Browse files
authored
Enforce max input length to the Ada URL parser (ada-url#1126)
* This commit introduces max input length controls to the Ada URL parser library, adding ada::set_max_input_length() and ada::get_max_input_length() functions for configurable limits (defaulting to 4GB) to prevent DoS attacks and excessive memory usage. Key changes include a new get_href_size() method for efficient size calculation without allocation, enforcement checks in all parsers and setters with automatic reversion on limit exceedance, and comprehensive tests including unit tests in max_input_length.cpp and a fuzzing simulation in max_length_fuzzer.cpp. The implementation uses thread-safe atomics, preserves ABI compatibility by only adding new functions, and covers edge cases like percent-encoding expansion and cumulative setter operations. * lint * various fixes * lint * saving a load * tuning * lint/clean
1 parent 67b4245 commit aaa4055

20 files changed

Lines changed: 1191 additions & 26 deletions

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,47 @@ url->set_hash("is-this-the-real-life"); // Update hash/fragment
179179
// url->get_hash() will return "#is-this-the-real-life"
180180
```
181181
182+
### URL Size Limit
183+
184+
By default, ada allows URLs up to about 4 GB. You can set a lower limit to
185+
reject any URL whose serialized form (the href) would exceed a given number
186+
of bytes. The limit is enforced during parsing and across all setters.
187+
Setters that return `bool` return `false` when the limit would be exceeded;
188+
`void` setters (`set_search`, `set_hash`) silently leave the URL unchanged.
189+
In all cases the URL is never modified when a limit violation is detected.
190+
Percent-encoding expansion is accounted for: a short input that encodes into
191+
a long result is still rejected.
192+
193+
```cpp
194+
// Set a 2 KB limit (any uint32_t value works).
195+
ada::set_max_input_length(2048);
196+
197+
// Parsing a URL whose normalized form exceeds 2 KB fails.
198+
auto url = ada::parse("http://example.com/" + std::string(2048, 'a'));
199+
assert(!url); // too long
200+
201+
// Setters that would push the URL over the limit are rejected.
202+
auto u = ada::parse<ada::url_aggregator>("http://example.com/");
203+
assert(u);
204+
bool ok = u->set_pathname(std::string(2048, 'x'));
205+
assert(!ok); // pathname too long; URL unchanged
206+
207+
// Read the current limit.
208+
uint32_t limit = ada::get_max_input_length();
209+
210+
// Reset to the default (no practical limit).
211+
ada::set_max_input_length(UINT32_MAX);
212+
```
213+
214+
You can query the byte length of a URL without allocating a string via `get_href_size()`:
215+
216+
```c++
217+
auto url = ada::parse<ada::url_aggregator>("https://example.com/path");
218+
assert(url);
219+
size_t len = url->get_href_size(); // no allocation
220+
assert(len == url->get_href().size());
221+
```
222+
182223
### URL Search Params
183224

184225
```cpp

benchmarks/bench_ipv4.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ void run_benchmark(benchmark::State& state,
139139
for (size_t i = 0; i < count; ++i) {
140140
auto result = ada::parse<ResultType>(urls[order[pos]]);
141141
if (result) {
142-
success++;
142+
success = success + 1;
143143
}
144144
benchmark::DoNotOptimize(result);
145145

@@ -161,7 +161,7 @@ void run_benchmark(benchmark::State& state,
161161
for (size_t j = 0; j < count; ++j) {
162162
auto result = ada::parse<ResultType>(urls[order[pos]]);
163163
if (result) {
164-
success++;
164+
success = success + 1;
165165
}
166166
benchmark::DoNotOptimize(result);
167167
pos += stride;

fuzz/max_length.cc

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
#include <fuzzer/FuzzedDataProvider.h>
2+
3+
#include <cassert>
4+
#include <cstdio>
5+
#include <limits>
6+
#include <string>
7+
#include <type_traits>
8+
9+
#include "ada.cpp"
10+
#include "ada.h"
11+
12+
// Enforce a tight limit and verify that no operation can produce
13+
// a URL whose serialized form exceeds it.
14+
static constexpr uint32_t kMaxLength = 512;
15+
16+
template <class T>
17+
static void check_length(const T& url, const char* context) {
18+
if (url.get_href_size() > kMaxLength) {
19+
printf("FAIL [%s]: href_size=%zu exceeds limit %u\n href: ", context,
20+
url.get_href_size(), kMaxLength);
21+
if constexpr (std::is_same_v<T, ada::url_aggregator>) {
22+
printf("%.*s\n", (int)url.get_href().size(), url.get_href().data());
23+
} else {
24+
printf("%s\n", url.get_href().c_str());
25+
}
26+
abort();
27+
}
28+
}
29+
30+
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
31+
ada::set_max_input_length(kMaxLength);
32+
33+
FuzzedDataProvider fdp(data, size);
34+
35+
// Consume strings for initial parse and setter values.
36+
std::string source = fdp.ConsumeRandomLengthString(1024);
37+
std::string base_str = fdp.ConsumeRandomLengthString(256);
38+
39+
// --- Test 1: parse must not produce an href > kMaxLength ---
40+
auto url = ada::parse<ada::url>(source);
41+
auto agg = ada::parse<ada::url_aggregator>(source);
42+
43+
if (url) {
44+
check_length(*url, "parse<url>");
45+
}
46+
if (agg) {
47+
check_length(*agg, "parse<url_aggregator>");
48+
}
49+
50+
// --- Test 2: parse with base ---
51+
auto base_url = ada::parse<ada::url>(base_str);
52+
auto base_agg = ada::parse<ada::url_aggregator>(base_str);
53+
54+
if (base_url) {
55+
auto result = ada::parse<ada::url>(source, &*base_url);
56+
if (result) {
57+
check_length(*result, "parse<url>(source, base)");
58+
}
59+
}
60+
if (base_agg) {
61+
auto result = ada::parse<ada::url_aggregator>(source, &*base_agg);
62+
if (result) {
63+
check_length(*result, "parse<url_aggregator>(source, base)");
64+
}
65+
}
66+
67+
// --- Test 3: setters on a known-good URL ---
68+
// Start from a short URL to maximise room for setter expansion.
69+
auto setter_url = ada::parse<ada::url>("http://x/");
70+
auto setter_agg = ada::parse<ada::url_aggregator>("http://x/");
71+
if (!setter_url || !setter_agg) return 0;
72+
73+
// Apply a fuzz-driven sequence of setter calls.
74+
int steps = fdp.ConsumeIntegralInRange(1, 16);
75+
for (int i = 0; i < steps && fdp.remaining_bytes() > 0; ++i) {
76+
std::string val = fdp.ConsumeRandomLengthString(512);
77+
int which = fdp.ConsumeIntegralInRange(0, 9);
78+
switch (which) {
79+
case 0:
80+
setter_url->set_protocol(val);
81+
setter_agg->set_protocol(val);
82+
break;
83+
case 1:
84+
setter_url->set_username(val);
85+
setter_agg->set_username(val);
86+
break;
87+
case 2:
88+
setter_url->set_password(val);
89+
setter_agg->set_password(val);
90+
break;
91+
case 3:
92+
setter_url->set_hostname(val);
93+
setter_agg->set_hostname(val);
94+
break;
95+
case 4:
96+
setter_url->set_host(val);
97+
setter_agg->set_host(val);
98+
break;
99+
case 5:
100+
setter_url->set_pathname(val);
101+
setter_agg->set_pathname(val);
102+
break;
103+
case 6:
104+
setter_url->set_search(val);
105+
setter_agg->set_search(val);
106+
break;
107+
case 7:
108+
setter_url->set_hash(val);
109+
setter_agg->set_hash(val);
110+
break;
111+
case 8:
112+
setter_url->set_port(val);
113+
setter_agg->set_port(val);
114+
break;
115+
case 9:
116+
setter_url->set_href(val);
117+
setter_agg->set_href(val);
118+
break;
119+
}
120+
check_length(*setter_url, "setter url");
121+
check_length(*setter_agg, "setter url_aggregator");
122+
}
123+
124+
// --- Test 4: aggressive percent-encoding expansion ---
125+
// Characters like spaces, control chars, and braces expand 3x when
126+
// percent-encoded. Try to overflow the limit via these characters.
127+
{
128+
auto pe_url = ada::parse<ada::url>("http://x/");
129+
auto pe_agg = ada::parse<ada::url_aggregator>("http://x/");
130+
if (pe_url && pe_agg) {
131+
std::string expanding = fdp.ConsumeRandomLengthString(512);
132+
pe_url->set_pathname(expanding);
133+
pe_agg->set_pathname(expanding);
134+
check_length(*pe_url, "percent-encode pathname url");
135+
check_length(*pe_agg, "percent-encode pathname url_aggregator");
136+
137+
pe_url->set_username(expanding);
138+
pe_agg->set_username(expanding);
139+
check_length(*pe_url, "percent-encode username url");
140+
check_length(*pe_agg, "percent-encode username url_aggregator");
141+
142+
pe_url->set_search(expanding);
143+
pe_agg->set_search(expanding);
144+
check_length(*pe_url, "percent-encode search url");
145+
check_length(*pe_agg, "percent-encode search url_aggregator");
146+
147+
pe_url->set_hash(expanding);
148+
pe_agg->set_hash(expanding);
149+
check_length(*pe_url, "percent-encode hash url");
150+
check_length(*pe_agg, "percent-encode hash url_aggregator");
151+
}
152+
}
153+
154+
// Reset to default so other tests/fuzzers are not affected.
155+
ada::set_max_input_length(std::numeric_limits<uint32_t>::max());
156+
return 0;
157+
}

include/ada/implementation.h

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ using result = tl::expected<result_type, ada::errors>;
6565
*
6666
* @note The parser is fully compliant with the WHATWG URL Standard.
6767
*
68+
* Parsing fails if the input or the resulting normalized URL exceeds
69+
* `get_max_input_length()` bytes (default ~4 GB, configurable via
70+
* `set_max_input_length()`). This accounts for percent-encoding expansion:
71+
* a short input that normalizes into a long URL is still rejected.
72+
*
6873
* @example
6974
* ```cpp
7075
* // Parse an absolute URL
@@ -104,6 +109,11 @@ extern template ada::result<url_aggregator> parse<url_aggregator>(
104109
* object. Use this when you only need to validate URLs without needing
105110
* their parsed components.
106111
*
112+
* When `get_max_input_length()` is set to a value smaller than the default,
113+
* `can_parse` may still return `true` for overlength inputs that are
114+
* structurally valid, because the fast path skips the length check for
115+
* performance. Use `parse()` when strict length enforcement is required.
116+
*
107117
* @param input The URL string to validate. Must be valid ASCII or UTF-8.
108118
* @param base_input Optional pointer to a base URL string for resolving
109119
* relative URLs. If nullptr (default), the input is validated as
@@ -166,6 +176,26 @@ parse_url_pattern(std::variant<std::string_view, url_pattern_init>&& input,
166176
* @return A file:// URL string representing the given path.
167177
*/
168178
std::string href_from_file(std::string_view path);
179+
180+
/**
181+
* Sets the maximum allowed length for URLs.
182+
*
183+
* Both the raw input and the resulting normalized URL (the href) are checked
184+
* against this limit. Parsing or setter calls that would produce a URL
185+
* exceeding this length are rejected. The value must fit in a uint32_t.
186+
* The default is std::numeric_limits<uint32_t>::max() (approximately 4 GB).
187+
*
188+
* @param length The new maximum URL length in bytes.
189+
*/
190+
void set_max_input_length(uint32_t length);
191+
192+
/**
193+
* Returns the current maximum allowed length for URLs.
194+
*
195+
* @return The current maximum URL length in bytes.
196+
*/
197+
uint32_t get_max_input_length();
198+
169199
} // namespace ada
170200

171201
#endif // ADA_IMPLEMENTATION_H

include/ada/url-inl.h

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ namespace ada {
2323
return port.has_value();
2424
}
2525
[[nodiscard]] inline bool url::cannot_have_credentials_or_port() const {
26-
return !host.has_value() || host.value().empty() ||
27-
type == ada::scheme::type::FILE;
26+
return !host.has_value() || host->empty() || type == ada::scheme::type::FILE;
2827
}
2928
[[nodiscard]] inline bool url::has_empty_hostname() const noexcept {
3029
if (!host.has_value()) {
@@ -217,6 +216,48 @@ constexpr void url::copy_scheme(const ada::url& u) {
217216
return output;
218217
}
219218

219+
[[nodiscard]] inline size_t url::get_href_size() const noexcept {
220+
// Mirrors the logic of get_href() but only computes the total size.
221+
size_t size = 0;
222+
// Protocol: scheme + ":"
223+
if (is_special()) {
224+
size += ada::scheme::details::is_special_list[type].size() + 1;
225+
} else {
226+
size += non_special_scheme.size() + 1;
227+
}
228+
if (host.has_value()) {
229+
size += host->size();
230+
size += 2; // "//"
231+
if (has_credentials()) {
232+
size += username.size();
233+
if (!password.empty()) {
234+
size += 1 + password.size(); // ":" + password
235+
}
236+
size += 1; // "@"
237+
}
238+
if (port.has_value()) {
239+
size += 1; // ":"
240+
// Count digits of port value without calling std::to_string.
241+
uint16_t p = port.value();
242+
size += (p >= 10000) ? 5
243+
: (p >= 1000) ? 4
244+
: (p >= 100) ? 3
245+
: (p >= 10) ? 2
246+
: 1;
247+
}
248+
} else if (!has_opaque_path && path.starts_with("//")) {
249+
size += 2; // "/."
250+
}
251+
size += path.size();
252+
if (query.has_value()) {
253+
size += 1 + query->size(); // "?" + query
254+
}
255+
if (hash.has_value()) {
256+
size += 1 + hash->size(); // "#" + hash
257+
}
258+
return size;
259+
}
260+
220261
ada_really_inline size_t url::parse_port(std::string_view view,
221262
bool check_trailing_content) noexcept {
222263
ada_log("parse_port('", view, "') ", view.size());

include/ada/url.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,12 @@ struct url : url_base {
162162
*/
163163
[[nodiscard]] ada_really_inline std::string get_href() const;
164164

165+
/**
166+
* Returns the byte length of the serialized URL without allocating a string.
167+
* @return Size of the href in bytes.
168+
*/
169+
[[nodiscard]] size_t get_href_size() const noexcept;
170+
165171
/**
166172
* Returns the URL's origin as a string (scheme + host + port for special
167173
* URLs).

include/ada/url_aggregator-inl.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -852,6 +852,10 @@ constexpr bool url_aggregator::has_port() const noexcept {
852852
return buffer;
853853
}
854854

855+
[[nodiscard]] constexpr size_t url_aggregator::get_href_size() const noexcept {
856+
return buffer.size();
857+
}
858+
855859
ada_really_inline size_t
856860
url_aggregator::parse_port(std::string_view view, bool check_trailing_content) {
857861
ada_log("url_aggregator::parse_port('", view, "') ", view.size());

include/ada/url_aggregator.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ struct url_aggregator : url_base {
104104
[[nodiscard]] constexpr std::string_view get_href() const noexcept
105105
ada_lifetime_bound;
106106

107+
/**
108+
* Returns the byte length of the serialized URL without allocating a string.
109+
* @return Size of the href in bytes.
110+
*/
111+
[[nodiscard]] constexpr size_t get_href_size() const noexcept;
112+
107113
/**
108114
* Returns the URL's username component.
109115
* Does not allocate memory. The returned view becomes invalid if this

include/ada_c.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,10 @@ ada_string_pair ada_search_params_entries_iter_next(
184184
bool ada_search_params_entries_iter_has_next(
185185
ada_url_search_params_entries_iter result);
186186

187+
// max URL length configuration
188+
void ada_set_max_input_length(uint32_t length);
189+
uint32_t ada_get_max_input_length(void);
190+
187191
// Definitions for Ada's version number.
188192
typedef struct {
189193
int major;

src/ada_c.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,14 @@ bool ada_search_params_entries_iter_has_next(
743743
return (*r)->has_next();
744744
}
745745

746+
void ada_set_max_input_length(uint32_t length) noexcept {
747+
ada::set_max_input_length(length);
748+
}
749+
750+
uint32_t ada_get_max_input_length() noexcept {
751+
return ada::get_max_input_length();
752+
}
753+
746754
typedef struct {
747755
int major;
748756
int minor;

0 commit comments

Comments
 (0)