Skip to content

Native static handler truncates the request path at a NUL byte (%00) and serves the prefix file #392

Description

@Guruprasanth-M

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions