Summary
clean_uri() in libks's HTTP request parser fails to reject URIs whose path has more segments than its internal canonicalization buffer can hold. The canonicalization step silently passes such URIs through with embedded ".." sequences intact, enabling path traversal in any consumer that later joins the URI with a filesystem path.
Details
clean_uri() in src/kws.c splits the URI on "/" into a 64-slot pointer array, then walks the array dropping "." segments and popping ".." segments to produce a canonical path. A size check is meant to refuse inputs deeper than the array:
if (argc == sizeof(argv)) { /* too deep */
return KS_STATUS_FAIL;
}
The comparison is incorrect: sizeof(argv) is the byte size of the pointer array (512 on 64-bit, 256 on 32-bit), not the element count (64). Because ks_separate_string() clamps its return value to the array's element count, the comparison is never true and the reject branch is dead code.
When the input exceeds 64 segments, ks_separate_string() stops splitting at the boundary and leaves the entire unsplit remainder - including its embedded "/" characters - in argv[63]. The canonicalization loop matches each slot against the literal two-character string "..", so a slot whose content is, for example, "a/b/../../etc/passwd" is not recognized as parent-directory tokens and is copied through verbatim. The reconstructed URI keeps every embedded ".." sequence intact. When a downstream consumer later concatenates this URI with a filesystem root and passes it to open(2), the kernel resolves the embedded ".." segments literally and escapes the intended directory.
Impact
A remote attacker who can submit URIs through a consumer of kws_parse_header() can read files outside any intended document root, provided:
- The URI carries at least 63 leading path segments (to exhaust the splitter's slot budget),
- The traversal payload follows in the unsplit tail,
- The consumer joins the returned URI with a filesystem path without further sanitization.
The canonical attack shape is GET /a/a/a/.../a/../../../../etc/passwd HTTP/1.1 with at least 63 padding segments - comfortably within typical HTTP-URI size limits.
Reachability in FreeSWITCH
The primary in-tree consumer is mod_verto's HTTP-static handler. The default FreeSWITCH configuration is not affected: stock verto.conf.xml ships with no <vhosts> block, and without that mod_verto's HTTP-static path is disabled and kws_parse_header() is never invoked for plain HTTP requests. Deployments that opt into HTTP-static serving by adding a <vhost> become reachable:
- If the
<vhost> has no auth-realm, the traversal is unauthenticated.
- If the
<vhost> has an auth-realm, the file-read code path is gated on Basic Auth, though clean_uri() itself still executes pre-auth.
Any other downstream that passes the URI from kws_parse_header() to a filesystem call inherits the same exposure.
Patches
The fix hoists the array's element count into a named constant and compares argc against it. A test in tests/testhttp.c now exercises kws_parse_header() end-to-end and asserts that >64-segment inputs are rejected.
Workarounds
Until the fix is deployed, operators can mitigate by:
- Leaving
<vhosts> disabled in verto.conf.xml (the default).
- If a vhost is required, configuring auth-realm so that file-serve and Lua-exec paths require authentication.
- Fronting the verto listener with a reverse proxy that rejects URIs containing ".." or with an abnormally large number of path segments.
Credit
Adam Bedard, Security Researcher.
Summary
clean_uri()in libks's HTTP request parser fails to reject URIs whose path has more segments than its internal canonicalization buffer can hold. The canonicalization step silently passes such URIs through with embedded ".." sequences intact, enabling path traversal in any consumer that later joins the URI with a filesystem path.Details
clean_uri()insrc/kws.csplits the URI on "/" into a 64-slot pointer array, then walks the array dropping "." segments and popping ".." segments to produce a canonical path. A size check is meant to refuse inputs deeper than the array:The comparison is incorrect:
sizeof(argv)is the byte size of the pointer array (512 on 64-bit, 256 on 32-bit), not the element count (64). Becauseks_separate_string()clamps its return value to the array's element count, the comparison is never true and the reject branch is dead code.When the input exceeds 64 segments,
ks_separate_string()stops splitting at the boundary and leaves the entire unsplit remainder - including its embedded "/" characters - inargv[63]. The canonicalization loop matches each slot against the literal two-character string "..", so a slot whose content is, for example, "a/b/../../etc/passwd" is not recognized as parent-directory tokens and is copied through verbatim. The reconstructed URI keeps every embedded ".." sequence intact. When a downstream consumer later concatenates this URI with a filesystem root and passes it toopen(2), the kernel resolves the embedded ".." segments literally and escapes the intended directory.Impact
A remote attacker who can submit URIs through a consumer of
kws_parse_header()can read files outside any intended document root, provided:The canonical attack shape is
GET /a/a/a/.../a/../../../../etc/passwd HTTP/1.1with at least 63 padding segments - comfortably within typical HTTP-URI size limits.Reachability in FreeSWITCH
The primary in-tree consumer is mod_verto's HTTP-static handler. The default FreeSWITCH configuration is not affected: stock
verto.conf.xmlships with no<vhosts>block, and without that mod_verto's HTTP-static path is disabled andkws_parse_header()is never invoked for plain HTTP requests. Deployments that opt into HTTP-static serving by adding a<vhost>become reachable:<vhost>has no auth-realm, the traversal is unauthenticated.<vhost>has an auth-realm, the file-read code path is gated on Basic Auth, thoughclean_uri()itself still executes pre-auth.Any other downstream that passes the URI from
kws_parse_header()to a filesystem call inherits the same exposure.Patches
The fix hoists the array's element count into a named constant and compares argc against it. A test in
tests/testhttp.cnow exerciseskws_parse_header()end-to-end and asserts that >64-segment inputs are rejected.Workarounds
Until the fix is deployed, operators can mitigate by:
<vhosts>disabled inverto.conf.xml(the default).Credit
Adam Bedard, Security Researcher.