Updated: Comment #N
Problem/Motivation
In Drupal 7 we had 2 protections against DoS attacks from people putting paths with a large number of parts into the Drupal URL and forcing the menu router to try to find them.
- We had the MENU_MAX_PARTS limit of 7 (later 9)
- We have the menu_masks from the variable which we use to filter out path patterns the do not exists in the router
In Drupal 8 we have removed both of these leaving us vulnerable. The problem is that the Drupal\Core\Routing\RouteProvider generates an IN() query whose length grows exponentially with the number of path parts.
Proposed resolution
record in State the max number of parts of a path - 404 immediately if there are more parts in the requested path.
reduce the length of the IN query. It seems that the current code is a carry-over when we allowed matching on a path that had unmatched trailing path parts. We can reduce this from a exponential number of parts to a linear number.
Remaining tasks
Write the code - test the 404 for an over-long path.
User interface changes
N/A
API changes
N/A
Comment | File | Size | Author |
---|---|---|---|
#28 | routing-2224777-28.patch | 26.63 KB | pwolanin |
#28 | increment.txt | 2.96 KB | pwolanin |
#21 | routing-2224777-15.patch | 25.49 KB | pwolanin |
#15 | routing-2224777-15.patch | 25.49 KB | pwolanin |
#15 | increment.txt | 5.14 KB | pwolanin |
Comments
Comment #1
dawehnerNot really. Let's have the following examples of routes:
In order to find the first one we don't store the {value} as placeholder, so '/some/path' also matches. One strategy certainly could be to
store both the optional and not optional elements in the DB and limit the lenght of the ancestors, though this seems to be worse how we do it at the moment.
Comment #2
pwolanin CreditAttribution: pwolanin commentedUpdating the summary after discussion with Daniel
Comment #3
dawehnerComment #5
dawehner.
Comment #7
pwolanin CreditAttribution: pwolanin commentedPer discussion with klausi:
The path column in the table should change to be the 1st path part.
we should create an index on num_parts and 1st path part
route name needs to change to an index, not unique.
For routes with optional trailing parts, we insert multiple rows, so we can match just on the number of parts in the request path.
The query to find the route matches the number of parts and the 1st path part (which can never be a wildcard).
This way we avoid the need to use state AND avoid the need to build menu masks to achieve even better performance at the cost of a few extra DB rows.
Comment #8
pwolanin CreditAttribution: pwolanin commentedWorking on this more, the plan above seems wrong. We should just go back to something like the masks from Drupal 7.
Updated patch attached.
Comment #10
pwolanin CreditAttribution: pwolanin commentedoops, let's not delete the path column yet.
Comment #12
klausiThis test case should also check the state variable routing.menu_masks after the dump() invocation to make sure it exists and is populated as expected.
So that does not really belong into this patch, because we are not doing the max parts thing yet.
Comment #13
pwolanin CreditAttribution: pwolanin commentedfixes test and fit logic.
Comment #14
klausiLooks very good!
The only thing missing is now a security test for this function: pass it a higher number of parts (15?) and check that it does not return a gazillion candidate outlines. That way we protect ourselves from future DoS regressions.
Comment #15
pwolanin CreditAttribution: pwolanin commentedHere's with added test cases. Also removed the check against max mask length - turns out that would exclude routes with trailing optional parts.
Comment #17
klausi15: routing-2224777-15.patch queued for re-testing.
Comment #19
dawehner15: routing-2224777-15.patch queued for re-testing.
Comment #21
pwolanin CreditAttribution: pwolanin commentedre-posting the patch so the last segfult is not obscured.
Comment #22
klausiDiscussed this extensively at the dev days sprint, and this looks good now. It covers the DoS security issue and brings back a nice performance improvement from D7.
Comment #23
pwolanin CreditAttribution: pwolanin commentedgreat, thanks
Comment #24
pwolanin CreditAttribution: pwolanin commentedComment #25
dawehner+1 for the current way. Further optimizations can come in.
Comment #26
catchThis is nice, I was concerned about the extra state get on every page, then saw this :)
Giving this a bit more time before commit, but no complaints from me.
Comment #27
Crell CreditAttribution: Crell commentedHow does it protect against that?
This shouldn't be an embedded if. Just another elseif.
These can be integer literals in 5.4 thanks to the new syntax: http://php.net/manual/en/migration54.new-features.php
0b10111 (etc.)
Going just by the patch, I have NFI what this code is doing or why. Without the issue summary or the explanation I just got in IRC, this is all just so much extra nonsense. That's why I removed it from the routing system when porting from the old one to the new one; I couldn't figure out what it was actually doing for functionality, I didn't need it, so I dropped it.
Before this goes in it MUST have better docs to explain what a mask is and WHY we're caching them and doing all of this stuff. As is, from the code I couldn't tell you which means it's going to get optimized out again by some well-meaning developer who doesn't know that it's there for DDOS reasons. There is absolutely nothing in the code or comments right now that even hints at DDOS as a reason.
Comment #28
pwolanin CreditAttribution: pwolanin commented@Crell - the replacement was an exponential O(2^n) algorithm, which should be obviously unacceptable. If you had NFI what it was doing, you could have asked.
I don't think the binary number format is clearer than bindec() - this is just test code, so not a performance issue.
Here's a patch with added comments and making the branch into an elseif
Comment #29
dawehnerNo matter what we do here, it can't get worse than it is now.
@crell
Just in case you want to reproduce that DDOS attack goto /node/1/foo/bar/baz/foo/bar/baz/foo/bar/baz/foo/bar/baz/foo/bar/baz/foo/bar/baz/foo/bar/baz/
Comment #30
catchCommitted/pushed to 8.x, thanks!