Native static handler truncates the request path at a NUL byte (%00) and serves the prefix file
Component: native static file handler (enable_static_handler)
OpenSwoole: 22.1.2 (ext, NTS) · PHP: 8.3.6 (NTS) · OS: Linux 6.8.0 (Ubuntu)
Severity: Medium — request-path NUL truncation; an extension/suffix-based access or content-type decision can be bypassed, and the response is served for a URL that differs from the resource actually returned.
Reproduces: every request, deterministic.
Summary
When enable_static_handler is on, a request path containing a percent-encoded NUL (%00) is decoded and the filesystem lookup is truncated at the NUL byte: GET /css/site.css%00.png returns the contents of /css/site.css with 200 OK. A conformant server (Apache, nginx) rejects a NUL in the decoded path with 400 Bad Request. The handler stays within document_root (it doesn't enable traversal), but it serves a file for a URL whose suffix/extension does not match what was returned.
Minimal reproduction (pure OpenSwoole)
<?php
$http = new OpenSwoole\HTTP\Server('127.0.0.1', 9700);
$http->set([
'document_root' => '/tmp/oswww', // contains css/site.css = "body{color:red}"
'enable_static_handler' => true,
'static_handler_locations' => ['/css'],
'worker_num' => 1,
]);
$http->on('request', function ($req, $resp) {
$resp->header('Content-Type', 'text/plain');
$resp->end("DYNAMIC handler saw: " . $req->server['request_uri'] . "\n");
});
$http->start();
curl -s -o /dev/null -w '%{http_code}\n' 'http://127.0.0.1:9700/css/site.css' # 200 (normal)
curl -s 'http://127.0.0.1:9700/css/site.css%00.png' # body{color:red} (!!)
curl -s 'http://127.0.0.1:9700/css/site.css%00EVIL' # body{color:red} (!!)
Raw socket (shows the truncation reaches the static handler, not the dynamic on('request')):
GET /css/site.css%00.png HTTP/1.1
-> HTTP/1.1 200 OK
Server: OpenSwoole 22.1.2
Content-Type: text/css
Content-Length: 15
<blank>
body{color:red}
Expected vs actual
| Request |
Apache / nginx |
OpenSwoole 22.1.2 |
GET /css/site.css%00.png |
400 Bad Request (NUL in path) |
200 OK, serves /css/site.css |
GET /css/site.css%00EVIL |
400 |
200, serves /css/site.css |
Spec / reference
RFC 3986 §2.1/§7.3 and the common server convention: a percent-encoded NUL (%00) in a request-target path is rejected (400). Apache's ap_unescape_url() returns HTTP_BAD_REQUEST on an embedded %00; nginx returns 400. A NUL must never be allowed to silently terminate the resolved filesystem path.
Impact
- Extension/suffix bypass: a check or routing decision keyed on the request suffix (
…\.png$, …\.css$) can be defeated — the URL ends in .png but /css/site.css is served.
- Cache / content-type confusion: caches key on the request URL (
/css/site.css%00.png) while the body/Content-Type come from /css/site.css.
- Bounded to
document_root + static_handler_locations (no path traversal observed), so this is hardening/correctness rather than arbitrary file read — but the NUL should still be rejected.
Suggested direction
In the static-handler path-decode (the URL-unescape step before the document_root join in swoole_http_server), reject a decoded path containing a \0 with 400 Bad Request (mirroring Apache ap_unescape_url), rather than passing the truncated C-string to the filesystem lookup.
Native static handler truncates the request path at a NUL byte (
%00) and serves the prefix fileComponent: native static file handler (
enable_static_handler)OpenSwoole: 22.1.2 (ext, NTS) · PHP: 8.3.6 (NTS) · OS: Linux 6.8.0 (Ubuntu)
Severity: Medium — request-path NUL truncation; an extension/suffix-based access or content-type decision can be bypassed, and the response is served for a URL that differs from the resource actually returned.
Reproduces: every request, deterministic.
Summary
When
enable_static_handleris on, a request path containing a percent-encoded NUL (%00) is decoded and the filesystem lookup is truncated at the NUL byte:GET /css/site.css%00.pngreturns the contents of/css/site.csswith200 OK. A conformant server (Apache, nginx) rejects a NUL in the decoded path with400 Bad Request. The handler stays withindocument_root(it doesn't enable traversal), but it serves a file for a URL whose suffix/extension does not match what was returned.Minimal reproduction (pure OpenSwoole)
Raw socket (shows the truncation reaches the static handler, not the dynamic
on('request')):Expected vs actual
GET /css/site.css%00.png400 Bad Request(NUL in path)200 OK, serves/css/site.cssGET /css/site.css%00EVIL400200, serves/css/site.cssSpec / reference
RFC 3986 §2.1/§7.3 and the common server convention: a percent-encoded NUL (
%00) in a request-target path is rejected (400). Apache'sap_unescape_url()returnsHTTP_BAD_REQUESTon an embedded%00; nginx returns400. A NUL must never be allowed to silently terminate the resolved filesystem path.Impact
…\.png$,…\.css$) can be defeated — the URL ends in.pngbut/css/site.cssis served./css/site.css%00.png) while the body/Content-Type come from/css/site.css.document_root+static_handler_locations(no path traversal observed), so this is hardening/correctness rather than arbitrary file read — but the NUL should still be rejected.Suggested direction
In the static-handler path-decode (the URL-unescape step before the
document_rootjoin inswoole_http_server), reject a decoded path containing a\0with400 Bad Request(mirroring Apacheap_unescape_url), rather than passing the truncated C-string to the filesystem lookup.