Skip to content

Commit abca372

Browse files
committed
api: decode query string values only once
`get_string_var()` ran `mg_url_decode()` over the value `mg_get_var()` returns, but `mg_get_var()` has already decoded it: we leave CivetWeb's `decode_query_string` at its default `no`, so the query string arrives raw and CivetWeb decodes it exactly once when extracting a variable. The second pass changed the value again. `?item=a%2541b` became `aAb` instead of `a%41b`, and any value containing a space or a `+` made `mg_url_decode()` return `-1`, so the parameter silently read as absent. This was harmless enough while `?item=` did not exist, but it is not any more: we now compare the (once-decoded) path item with the (twice-decoded) query item and answer 400 when they disagree, so a client naming a group with a `%` in it got its request rejected. Return what `mg_get_var()` gives us. Variable *names* are still matched against the raw query string, so the percent-encoded names in `queries.c` (e.g. `order%5B0%5D%5Bdir%5D`) keep working. Adds pytest coverage for both cases: a group whose name contains `%` addressed through path and `?item=` at once, and one containing a space addressed through `?item=` alone. Signed-off-by: DL6ER <dl6er@dl6er.de>
1 parent 5e5338c commit abca372

2 files changed

Lines changed: 36 additions & 24 deletions

File tree

src/webserver/http-common.c

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -411,30 +411,12 @@ int get_string_var(const char *source, const char *var, char *dest, size_t dest_
411411
if(!source)
412412
return -1;
413413

414-
// Allocate a temporary buffer to store the possibly URI-encoded value
415-
// of the variable. We use the real destination later to store the
416-
// decoded value. The decoded value will always be shorter than the
417-
// encoded value, so using the same length is fine.
418-
char *tempbuf = calloc(dest_len, sizeof(char));
419-
if(!tempbuf)
420-
{
421-
log_err("get_string_var: Out of memory");
422-
return -1;
423-
}
424-
425-
// Extract value of the particular variable
426-
int len = mg_get_var(source, strlen(source), var, tempbuf, dest_len);
427-
428-
// Decode the URI component if needed
429-
if(len > 0)
430-
len = mg_url_decode(tempbuf, len, dest, dest_len, 0);
431-
432-
// Free the temporary buffer, if anything was decoded it's now stored in
433-
// dest
434-
free(tempbuf);
435-
436-
// Return the length of the decoded string
437-
return len;
414+
// Extract the value of the particular variable. mg_get_var() decodes it
415+
// for us - the query string reaches us as it was sent because CivetWeb's
416+
// decode_query_string is left at its default "no". Decoding a second
417+
// time here would corrupt every value containing a literal '%' and let
418+
// one containing a space or '+' fail altogether.
419+
return mg_get_var(source, strlen(source), var, dest, dest_len);
438420
}
439421

440422
const char* __attribute__((pure)) startsWith(const char *path, struct ftl_conn *api)

test/api/test_m_mutations.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,36 @@ def test_put_group_with_conflicting_item_query_returns_400(self, api_session):
546546
assert r.status_code == 400, \
547547
f"Expected 400, got {r.status_code} {r.text}"
548548

549+
def test_put_and_delete_group_with_percent_in_name(self, api_session):
550+
"""A '%' survives: path and ?item= are both decoded exactly once."""
551+
name = "_pytest_%41_group"
552+
url = f"{FTL_URL}/api/groups/{quote(name)}?item={quote(name)}"
553+
554+
r = api_session.put(url,
555+
json={"comment": "pytest percent", "enabled": True},
556+
timeout=10)
557+
assert r.status_code in (200, 201), f"PUT failed: {r.status_code} {r.text}"
558+
assert _j(r)["groups"][0]["name"] == name
559+
560+
# Clean up
561+
r = api_session.delete(url, timeout=10)
562+
assert r.status_code == 204
563+
564+
def test_put_and_delete_group_with_space_via_item_query(self, api_session):
565+
"""A space in ?item= is part of the value, not a decoding error."""
566+
name = "_pytest space group"
567+
url = f"{FTL_URL}/api/groups?item={quote(name)}"
568+
569+
r = api_session.put(url,
570+
json={"comment": "pytest space", "enabled": True},
571+
timeout=10)
572+
assert r.status_code in (200, 201), f"PUT failed: {r.status_code} {r.text}"
573+
assert _j(r)["groups"][0]["name"] == name
574+
575+
# Clean up
576+
r = api_session.delete(url, timeout=10)
577+
assert r.status_code == 204
578+
549579

550580
# ===========================================================================
551581
# Batch delete tests

0 commit comments

Comments
 (0)