-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
Use the PCRE_DOLLAR_ENDONLY modifier in route regexes #25373
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
@nicolas-grekas Do you have a good hint for me why the |
The change impacts two components, but composer.json allows a non patched version of one of them to be used with the patched version of the other one. That's the reason. Is there any way to make this change in a different way that doesn't needs changing the regexps? Like in the base matcher? |
* also works in later symfony versions, i.e. the unserialized route is in the | ||
* same state as another, semantically equivalent, route. | ||
*/ | ||
public function testSerializedRepresentationKeepsWorking() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this test should be updated, not removed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sorry I removed it without comment.
I don't see how this test makes sense in its current state... It checks that a Route that has been created and serialized with a former release/version of Symfony (which one?) can be unserialized and actually equals the Route, including the regexp, that the current version would generate.
This obviously fails as the regexp is changed in this PR. Fiddling with the serialized string to have it reflect the "new" regexp cannot be a solution either.
Would it suffice to check that the unserialize
call itself works?
Maybe @Tobion you could explain what you intended to assert with this test?
@nicolas-grekas Thanks for the hint! I have no clever idea ATM how to achieve that... :-( Using for example Asking the other way round: If a change in one component breaks tests in another one, maybe that's an opportunity to re-organize/re-structure the affected tests? This somehow smells, doesn't it? |
On other side, changing existing tests is a sign of a BC break... |
Not necessarily, if the tests are too implementation specific and do not only guard intended behaviour. But let's not argue here 🍻. Handling routes with a trailing, urlencoded newline is an edge case. I happened to discover this because someone requested such URLs on one of our apps and I found notices in the PHP log. IMO the "right" fix would be to make PHP not silently ignore the newline at the end of the string, which I think is quirky PHP behavior in the first place. The rest is up to you guys to decide, and as usual, you're gonna make a good decision 😇. |
* also works in later symfony versions, i.e. the unserialized route is in the | ||
* same state as another, semantically equivalent, route. | ||
*/ | ||
public function testSerializedRepresentationKeepsWorking() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this test should be removed entirely.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@stof – yes, please see Tobias' remark and my response above.
@nicolas-grekas If I get you right, the problem is that this PR needs to change the Routing component and tests/fixtures in FrameworkBundle? The Descriptor tests in FrameworkBundle are brittle, as they fail when the route regexp changes. IMO it's not the intention of these So, what if the Descriptor tests allowed placeholders/regexes in the fixtures to accept (some) changes in the output? |
…s in the UrlMatcher (mpdude) This PR was squashed before being merged into the 2.7 branch (closes #25427). Discussion ---------- Preserve percent-encoding in URLs when performing redirects in the UrlMatcher | Q | A | ------------- | --- | Branch? | 2.7 | Bug fix? | yes | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | | License | MIT | Doc PR | While investigating #25373, I found that when the *dumped* `UrlMatcher` performs redirections due to missing trailing slashes on URLs, it does so using an url*de*coded URL. This is wrong, as it may lead to wrong interpretations of URLs upon the next request. For example, think of an URL that contains `%23` in the middle of the path info. Upon redirection, this will be turned into `#` with an obvious effect. Commits ------- 8146510 Preserve percent-encoding in URLs when performing redirects in the UrlMatcher
Ping @symfony/deciders I‘d like to know whether you’re going to pull/fix this. I am continuously getting the notices caused by this in my log monitoring. If you won’t fix it, I’ll have a look what my other options are. I haven’t heard any objections against the fix per se. The issue is that it breaks some tests across components. The tests affected IMO never intended to assert a particular routing behavior and do so only by accident. |
Continued in #26035 |
@nicolas-grekas I probably don’t fully understand the implications of what I am suggesting here. Do we agree that the tests for Even more, agree that the regex used is an implementation detail whereas tests should assert behavior? Is it even impossible to change Could we address this by narrowing dependency version constraints on either component/bundle? Or isn’t that an option either because we cannot change them for already released versions (that is, preventing tests from “before” the change to run against a version containing the change)? Thanks for your help, I appreciate your feedback! |
Yes about FrameworkBundle. Narrowing minimum version is possible, but maybe better not and make tests less strict instead. |
Ok, I don’t get this serialized routes part... you mean people have tucked away serialized routes somewhere and you would like them to continue working as-is? Why would this PR break that? |
I’d like to look into making FrameworkBundle tests less strict if you say that this might solve the problem. I guess that would still require bumping deps just to make sure the tests are liberal enough? |
$collection = new RouteCollection(); | ||
$collection->add('foo', new Route('/foo')); | ||
|
||
$matcher = new UrlMatcher($collection, new RequestContext()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should use the new getUrlMatcher method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I seem to be unable to find a getUrlMatcher
method somewhere...?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
missing update
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when this is fixed, I'll be +1 :)
*/ | ||
public function testSerializedRepresentationKeepsWorking() | ||
{ | ||
$serialized = 'C:31:"Symfony\Component\Routing\Route":934:{a:8:{s:4:"path";s:13:"/prefix/{foo}";s:4:"host";s:20:"{locale}.example.net";s:8:"defaults";a:1:{s:3:"foo";s:7:"default";}s:12:"requirements";a:1:{s:3:"foo";s:3:"\d+";}s:7:"options";a:1:{s:14:"compiler_class";s:39:"Symfony\Component\Routing\RouteCompiler";}s:7:"schemes";a:0:{}s:7:"methods";a:0:{}s:8:"compiled";C:39:"Symfony\Component\Routing\CompiledRoute":569:{a:8:{s:4:"vars";a:2:{i:0;s:6:"locale";i:1;s:3:"foo";}s:11:"path_prefix";s:7:"/prefix";s:10:"path_regex";s:30:"#^/prefix(?:/(?P<foo>\d+))?$#s";s:11:"path_tokens";a:2:{i:0;a:4:{i:0;s:8:"variable";i:1;s:1:"/";i:2;s:3:"\d+";i:3;s:3:"foo";}i:1;a:2:{i:0;s:4:"text";i:1;s:7:"/prefix";}}s:9:"path_vars";a:1:{i:0;s:3:"foo";}s:10:"host_regex";s:39:"#^(?P<locale>[^\.]++)\.example\.net$#si";s:11:"host_tokens";a:2:{i:0;a:2:{i:0;s:4:"text";i:1;s:12:".example.net";}i:1;a:4:{i:0;s:8:"variable";i:1;s:0:"";i:2;s:7:"[^\.]++";i:3;s:6:"locale";}}s:9:"host_vars";a:1:{i:0;s:6:"locale";}}}}}'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just add the missing Documents and keep the test
I don't think so. Let's try without. |
you need to fetch+rebase on lastest 2.7 |
1404434
to
61a8840
Compare
@nicolas-grekas Using a RouteStub in FrameworkBundle tests, kept the Route unserialization test, rebased & squashed. Let's 🤞 and see if tests stay 🆗 Update: fabbot.io complains about fixture for generated code violating CS; ignoring that. |
@@ -272,7 +272,7 @@ public function testSerializeWhenCompiled() | ||
*/ | ||
public function testSerializedRepresentationKeepsWorking() | ||
{ | ||
$serialized = 'C:31:"Symfony\Component\Routing\Route":934:{a:8:{s:4:"path";s:13:"/prefix/{foo}";s:4:"host";s:20:"{locale}.example.net";s:8:"defaults";a:1:{s:3:"foo";s:7:"default";}s:12:"requirements";a:1:{s:3:"foo";s:3:"\d+";}s:7:"options";a:1:{s:14:"compiler_class";s:39:"Symfony\Component\Routing\RouteCompiler";}s:7:"schemes";a:0:{}s:7:"methods";a:0:{}s:8:"compiled";C:39:"Symfony\Component\Routing\CompiledRoute":569:{a:8:{s:4:"vars";a:2:{i:0;s:6:"locale";i:1;s:3:"foo";}s:11:"path_prefix";s:7:"/prefix";s:10:"path_regex";s:30:"#^/prefix(?:/(?P<foo>\d+))?$#s";s:11:"path_tokens";a:2:{i:0;a:4:{i:0;s:8:"variable";i:1;s:1:"/";i:2;s:3:"\d+";i:3;s:3:"foo";}i:1;a:2:{i:0;s:4:"text";i:1;s:7:"/prefix";}}s:9:"path_vars";a:1:{i:0;s:3:"foo";}s:10:"host_regex";s:39:"#^(?P<locale>[^\.]++)\.example\.net$#si";s:11:"host_tokens";a:2:{i:0;a:2:{i:0;s:4:"text";i:1;s:12:".example.net";}i:1;a:4:{i:0;s:8:"variable";i:1;s:0:"";i:2;s:7:"[^\.]++";i:3;s:6:"locale";}}s:9:"host_vars";a:1:{i:0;s:6:"locale";}}}}}'; | ||
$serialized = 'C:31:"Symfony\Component\Routing\Route":959:{a:9:{s:4:"path";s:13:"/prefix/{foo}";s:4:"host";s:20:"{locale}.example.net";s:8:"defaults";a:1:{s:3:"foo";s:7:"default";}s:12:"requirements";a:1:{s:3:"foo";s:3:"\d+";}s:7:"options";a:1:{s:14:"compiler_class";s:39:"Symfony\Component\Routing\RouteCompiler";}s:7:"schemes";a:0:{}s:7:"methods";a:0:{}s:9:"condition";s:0:"";s:8:"compiled";C:39:"Symfony\Component\Routing\CompiledRoute":571:{a:8:{s:4:"vars";a:2:{i:0;s:6:"locale";i:1;s:3:"foo";}s:11:"path_prefix";s:7:"/prefix";s:10:"path_regex";s:31:"#^/prefix(?:/(?P<foo>\d+))?$#sD";s:11:"path_tokens";a:2:{i:0;a:4:{i:0;s:8:"variable";i:1;s:1:"/";i:2;s:3:"\d+";i:3;s:3:"foo";}i:1;a:2:{i:0;s:4:"text";i:1;s:7:"/prefix";}}s:9:"path_vars";a:1:{i:0;s:3:"foo";}s:10:"host_regex";s:40:"#^(?P<locale>[^\.]++)\.example\.net$#sDi";s:11:"host_tokens";a:2:{i:0;a:2:{i:0;s:4:"text";i:1;s:12:".example.net";}i:1;a:4:{i:0;s:8:"variable";i:1;s:0:"";i:2;s:7:"[^\.]++";i:3;s:6:"locale";}}s:9:"host_vars";a:1:{i:0;s:6:"locale";}}}}}'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should only add the "D" to the regexp, and NOT add anything else (eg not the "condition" property), because the purpose of the test is to ensure we can still unserialize old routes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$serialized = 'C:31:"Symfony\Component\Routing\Route":936:{a:8:{s:4:"path";s:13:"/prefix/{foo}";s:4:"host";s:20:"{locale}.example.net";s:8:"defaults";a:1:{s:3:"foo";s:7:"default";}s:12:"requirements";a:1:{s:3:"foo";s:3:"\d+";}s:7:"options";a:1:{s:14:"compiler_class";s:39:"Symfony\Component\Routing\RouteCompiler";}s:7:"schemes";a:0:{}s:7:"methods";a:0:{}s:8:"compiled";C:39:"Symfony\Component\Routing\CompiledRoute":571:{a:8:{s:4:"vars";a:2:{i:0;s:6:"locale";i:1;s:3:"foo";}s:11:"path_prefix";s:7:"/prefix";s:10:"path_regex";s:31:"#^/prefix(?:/(?P<foo>\d+))?$#sD";s:11:"path_tokens";a:2:{i:0;a:4:{i:0;s:8:"variable";i:1;s:1:"/";i:2;s:3:"\d+";i:3;s:3:"foo";}i:1;a:2:{i:0;s:4:"text";i:1;s:7:"/prefix";}}s:9:"path_vars";a:1:{i:0;s:3:"foo";}s:10:"host_regex";s:40:"#^(?P<locale>[^\.]++)\.example\.net$#sDi";s:11:"host_tokens";a:2:{i:0;a:2:{i:0;s:4:"text";i:1;s:12:".example.net";}i:1;a:4:{i:0;s:8:"variable";i:1;s:0:"";i:2;s:7:"[^\.]++";i:3;s:6:"locale";}}s:9:"host_vars";a:1:{i:0;s:6:"locale";}}}}}';
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nicolas-grekas Are you sure that this is correct? unserialize
fails for me... Could you pastebin this somewhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated above, some numbers missed an update
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Works – thanks!
$collection = new RouteCollection(); | ||
$collection->add('foo', new Route('/foo')); | ||
|
||
$matcher = new UrlMatcher($collection, new RequestContext()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when this is fixed, I'll be +1 :)
Thank you @mpdude. |
…ude) This PR was squashed before being merged into the 2.7 branch (closes #25373). Discussion ---------- Use the PCRE_DOLLAR_ENDONLY modifier in route regexes | Q | A | ------------- | --- | Branch? | 2.7 | Bug fix? | yes | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | | License | MIT | Doc PR | `UrlMatcher::match($pathinfo)` applies `rawurldecode()` to the `$pathinfo` before trying to match it against the routes. If the URL contains a percent-encoded trailing newline (like in `/foo%0a`), the default PHP PCRE will still consider `#^/foo$#` a match, as the `$` metacharacter will also match *immediately before* the final character *if it is a newline*. This behavior can be changed by applying the [`PCRE_DOLLAR_ENDONLY` modifier](http://php.net/manual/en/reference.pcre.pattern.modifiers.php). Without this change, URLs with trailing `%0a` lead to weird notices further down the road, for example when the `RedirectableUrlMatcher` or its equivalent in `PhpMatcherDumper` kick in, look at the last character (this time actually the newline), append a `/` and try to redirect to the resulting URL. Ultimately, PHP will complain with `Warning: Header may not contain more than a single header, new line detected` when sending the `Location` header. Commits ------- f713a3e Use the PCRE_DOLLAR_ENDONLY modifier in route regexes
@nicolas-grekas Thank you for you help! Your professionalism and solution-oriented approach makes it always fun working with you. |
UrlMatcher::match($pathinfo)
appliesrawurldecode()
to the$pathinfo
before trying to match it against the routes.If the URL contains a percent-encoded trailing newline (like in
/foo%0a
), the default PHP PCRE will still consider#^/foo$#
a match, as the$
metacharacter will also match immediately before the final character if it is a newline. This behavior can be changed by applying thePCRE_DOLLAR_ENDONLY
modifier.Without this change, URLs with trailing
%0a
lead to weird notices further down the road, for example when theRedirectableUrlMatcher
or its equivalent inPhpMatcherDumper
kick in, look at the last character (this time actually the newline), append a/
and try to redirect to the resulting URL. Ultimately, PHP will complain withWarning: Header may not contain more than a single header, new line detected
when sending theLocation
header.