Skip to content

ext/uri: Uri\WhatWg\Url::parse() treats "?"/"#" after a dot segment as path ("/..#frag" parses as path "/frag" with no fragment) #23635

Description

@aharsani

Description

Uri\WhatWg\Url::parse() mis-parses a URL whose path ends in a dot segment (., .., or percent-encoded %2e%2e) immediately followed by ? or #. The ?/# and everything after it are appended to the path as a new segment instead of becoming the query/fragment.

<?php
foreach ([
    "https://example.com/..#frag",
    "https://example.com/.#frag",
    "https://example.com/a/..#frag",
    "https://example.com/%2e%2e#frag",
    "https://example.com/..?q=1",
    "https://example.com/a/../b#frag", // control: works
] as $input) {
    $url = Uri\WhatWg\Url::parse($input);
    printf("%-34s path=%-8s query=%-6s fragment=%-6s serialized=%s\n",
        $input, var_export($url->getPath(), true), var_export($url->getQuery(), true),
        var_export($url->getFragment(), true), $url->toAsciiString());
}

Resulted in this output:

https://example.com/..#frag        path='/frag'  query=NULL   fragment=NULL   serialized=https://example.com/frag
https://example.com/.#frag         path='/frag'  query=NULL   fragment=NULL   serialized=https://example.com/frag
https://example.com/a/..#frag      path='/frag'  query=NULL   fragment=NULL   serialized=https://example.com/frag
https://example.com/%2e%2e#frag    path='/frag'  query=NULL   fragment=NULL   serialized=https://example.com/frag
https://example.com/..?q=1         path='/q=1'   query=NULL   fragment=NULL   serialized=https://example.com/q=1
https://example.com/a/../b#frag    path='/b'     query=NULL   fragment='frag' serialized=https://example.com/b#frag

But I expected this output instead (per the WHATWG URL Standard, path state: on ? or # the buffer is processed as a single/double-dot segment and the state switches to query state / fragment state; https://url.spec.whatwg.org/#path-state):

https://example.com/..#frag        path='/'      query=NULL   fragment='frag' serialized=https://example.com/#frag
https://example.com/.#frag         path='/'      query=NULL   fragment='frag' serialized=https://example.com/#frag
https://example.com/a/..#frag      path='/'      query=NULL   fragment='frag' serialized=https://example.com/#frag
https://example.com/%2e%2e#frag    path='/'      query=NULL   fragment='frag' serialized=https://example.com/#frag
https://example.com/..?q=1         path='/'      query='q=1'  fragment=NULL   serialized=https://example.com/?q=1
https://example.com/a/../b#frag    path='/b'     query=NULL   fragment='frag' serialized=https://example.com/b#frag

That expected output is what browsers and Node's WHATWG URL produce, e.g. new URL("https://example.com/..#frag") gives pathname "/", hash "#frag". Uri\Rfc3986\Uri::parse("https://example.com/..#frag") in the same PHP build also returns path '/', fragment 'frag'. The last input shows the dot segment itself is handled correctly when it is followed by /; the failure is specific to ? and # directly after the dot segment, which suggests the bundled Lexbor URL parser processes the dot-segment buffer but does not switch to the query/fragment state on those two characters.

Security note: code that relies on getFragment()/getQuery() to reject URLs carrying a fragment or query (for example an OAuth redirect allow-list that later appends its own ?code=...) accepts these inputs, while the browser that receives the URL treats the remainder as a fragment. Found while hardening such a redirect service.

PHP Version

PHP 8.5.10 (official php:8.5-fpm / php:8.5-cli Docker images). Also reproduces on PHP 8.6.0beta2 (php:8.6-rc-cli). php --ri uri: URI support => active.

Operating System

Debian (official Docker images), linux/arm64 and amd64.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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