Skip to content

[WIP] Do not expose routing 404 exception for protected route #11480

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

Closed
wants to merge 1 commit into from

Conversation

wouterj
Copy link
Member

@wouterj wouterj commented Jul 26, 2014

Q A
Bug fix? yes
New feature? no
BC breaks? no
Deprecations? no
Tests pass? yes (did fail locally though...)
Fixed tickets #2679
License MIT
Doc PR not related

I'm not sure if this is seen as a new feature or as a bug fix, so I just
submitted it to the master branch.

@wouterj wouterj changed the title Do not expose routing 404 exception for protected route [WIP] Do not expose routing 404 exception for protected route Jul 26, 2014
@wouterj
Copy link
Member Author

wouterj commented Jul 26, 2014

Do not merge this yet, I've found a big problem with this PR :)

*/
public function testRoutingErrorIsNotExposedForNotExistingProtectedResource($config)
{
if (strpos(PHP_OS, "WIN") === 0 && version_compare(phpversion(), "5.3.9", "<")) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Symfony code usually checks if the OS is Windows using this snippet:

if (defined('PHP_WINDOWS_VERSION_BUILD')) {
    // ...
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just copied this from the test above this one, should I change it everywhere?

@weaverryan
Copy link
Member

I know this is a difficult issue to solve, but from a user-perspective, I hope this will work out :).

public function onKernelException(GetResponseForExceptionEvent $event)
{
$exception = $event->getException();
if ($exception instanceof HttpException && 404 === $exception->getStatusCode()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you might be able to use Response::HTTP_NOT_FOUND not found here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The framework normally uses the status codes directly afair.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@apfelbox you are right. See #11401 as an example.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, sorry for noise

@gquemener
Copy link
Contributor

I don't get the purpose of this PR.

Currently, when an access control is set on a path that is not defined in the routing it will return a 401 response when trying to access it without the correct permission, instead of a 404?

@apfelbox
Copy link
Contributor

@gquemener yes. In a protected area, you should always return a 401.
Otherwise you can find out existing URLs by just brute forcing HTTP requests against some URLs (you can check whether you will receive a 404 oder 401).

The URL might contain (in some way) sensitive data, so this is a privacy issue.

@linaori
Copy link
Contributor

linaori commented Jul 29, 2014

If I understand the case correctly, you're effectively trying to change the exception into an access denied based on the URL and permissions.

Checking kernel.exception is already too late. What you want to do is change the exception being thrown by the routing if access is denied.

@gquemener
Copy link
Contributor

I'm quite confused here.
The access checking is performed on the kernel request event which occurs before the controller corresponding to the route is resolved (which throw the 404 when it cannot be resolved).

In that case, I don't see how a 404 could be returned when the firewall detects an unauthorized access.

@wouterj
Copy link
Member Author

wouterj commented Jul 29, 2014

In that case, I don't see how a 404 could be returned when the firewall detects an unauthorized access.

It's not about the 404 errors thrown by a controller. When a router can't find a route, it'll throw a 404 causing exception too. Since the firewall is registered after the router (so we can use routes for the login path and such), this exception will be thrown first and the firewall never gets executed. That's why people see the 404 exception. We have to find a way to transform those 404 exceptions into 403 exceptions.

Checking kernel.exception is already too late. What you want to do is change the exception being thrown by the routing if access is denied.

We can't change the routing exception, since the Routing shouldn't do anything with security. And we can't place it before the router, since we loose the ability to use routes then.

@gquemener
Copy link
Contributor

Ah allright, thanks @wouterj, I forgot about this RouterListener.

Crazy idea: Forbid to set access_control on route that does not exist.

@wouterj
Copy link
Member Author

wouterj commented Jul 29, 2014

Crazy idea: Forbid to set access_control on route that does not exist.

That's not going to solve the issue. Assume an access_control like: ^/admin

Assume the admin can edit blog articles. That URL would be something like /admin/blog/post/{id}/edit. Now, assume a post with id 200 does not exists (since it only has 50 blog posts), you get a 404 error when visiting /admin/blog/post/200/edit. You should get a 401 error. This way, you can find out exactly how many posts are not yet published.
That isn't that problematic, but there might be more private URLs too.

@gquemener
Copy link
Contributor

In the example you give,
if you have set an access_control on ^/admin and you try to access /admin/blog/post/200/edit without the correct access attributes, you'll get a 401 (because the route/admin/blog/post/{id}/edit has been defined in the routing).

The only case that is not handled correctly today is when setting an access_control with a pattern that doesn't match any route in the application. I'm wondering if it makes sense to set access control with such pattern.

Doesn't it mean that one protects access of... well... nothing?

@wouterj
Copy link
Member Author

wouterj commented Jul 29, 2014

Then I don't understand what @schmittjoh tried to say in #2679 ...

@gquemener
Copy link
Contributor

I think you clarified very well the issue in #2679 (comment)

I'm just concerned about the real use case of defining access rule on pattern that will never be matched against any route in the application.

If there's no such use case, then it'll probably be easier to prevent user to configure access rules on invalid route pattern, than trying to convert 404 into 403.

@wouterj
Copy link
Member Author

wouterj commented Jul 29, 2014

@gquemener I got it now. If you define an access rule for ^/admin and you have a controller for /admin/settings/general, but not for /admin/settings/specific, you get a 404 error when visiting /admin/settings/specific, while a 401 was expected.

@gquemener
Copy link
Contributor

Aaah that's right, that's why my solution is not acceptable! Good catch @wouterj!

So, it seems that your solution is quite acceptable then. Let me know if you need some help with "the BIG problem with this PR" 😉 (see #11480 (comment))

@wouterj
Copy link
Member Author

wouterj commented Jul 29, 2014

So, it seems that your solution is quite acceptable then. Let me know if you need some help with "the BIG problem with this PR" 😉

The problem:

  • The AccessDeniedException is transformed to a 401 result code by the ExceptionListener of the Security component
  • This ExceptionListener is only registered when the firewall is executed (in onKernelRequest)
  • When the router cannot find the route, the firewall will not be executed
  • We then get into the exception event and onKernelException will be executed
  • This executes the firewall, which wants to add the ExceptionListener to the kernel.exception event
  • Since we are already in that event, the EventDispatcher fails.

So there is no way we can handle the AccessDeniedException in kernel.exception...

@linaori
Copy link
Contributor

linaori commented Jul 29, 2014

I was having a BrainstormSessionInterface with myself so here we go.

At the moment, we rely on an Exception to be thrown when a 404 is triggered. Because an exception is thrown, the kernel.exception is triggered. This prevents the framework to continue it's normal event flow and goes into error mode. We then have listeners to handle what's going on, a 404 might show a page or redirect. A 401 will forward to login. A 403 will show an access denied page. We have a custom listener that initializes symfony1 as fallback.

What if, instead of throwing an exception, we fire a new event (example: kernel.resource_not_found). This gives a greater flexibility and allows us to do the following with listeners:

  • prio 0: 404 handler > throws a 404 exception to handle the current kernel.exception, default as it is now
  • prio 10: 403 handler, checks permissions on that route (^/admin for example) > throws a 403 exception to deny access
  • prio 20: 401 handler, checks authentication > throws a 401 exception to deny access
  • My custom Listener: prio 1, initialize symfony1, return content or continue on 404.

This solves a couple of things for symfony as you won't have your dependency issue between the routing and security. It will also allow for stronger fallbacks which additionally, for my personal case, we can use our (by symfony2 defined) error pages if symfony1 also goes into 404 because we can still access the proper chain. Right now we listen to kernel.exception which already is going exit in symfony2.

As far as I can tell, this is only an internal change. The 404 exception will still be thrown at lowest priority and go into the kernel.exception state, thus not causing a BC break. The only possible BC break I can imagine is the additional dependency on the event dispatcher.

I hope I managed to make clear what my idea is, if you have any questions, feel free to ask.

@stof
Copy link
Member

stof commented Aug 14, 2014

@iltar triggering an event would not prevent other logic from running, while you might not be in a state where you want it to run (and all places wanting to trigger a 404 would then have to be able to return a Response making its way all the way up to the kernel).

@linaori
Copy link
Contributor

linaori commented Aug 14, 2014

@stof but in this issue, that behaviour is actually desired. You can still throw the 404 exception, this won't be backwards incompatible. The event would also trigger the 404 exception (rather than setting content in the event).

The only difference is that using an event, you can hook into it and do some custom checks, for example alter the status from 404 to 403 or 401, without BC breaks.

@stof
Copy link
Member

stof commented Aug 14, 2014

@iltar if you register a listener on the kernel.exception event with a higher priority than the the ExceptionListener rendering the error page, you can already alter things. this is how the Firewall handles the AccessDeniedException, by redirecting to the login page or replacing the exception with an AccessDeniedHttpException (which will render a 403 in the default listener) depending on whether you are authenticated or no

@fabpot
Copy link
Member

fabpot commented Sep 15, 2014

@wouterj What's the status of this PR? I think it's not worth to invest too much into this.

@wouterj
Copy link
Member Author

wouterj commented Sep 15, 2014

@fabpot this can't be done with the current code design. I kept this PR open because there is an interesting discussion going on about how to fix this issue. I think it does make a lot of sense to at least try to fix this before Symfony 3.0.

@fabpot
Copy link
Member

fabpot commented Sep 15, 2014

@wouterj "this can't be done with the current code design." Sure, that's why I didn't fix it by then. So, closing now.

@fabpot fabpot closed this Sep 15, 2014
@wouterj wouterj deleted the issue_2679 branch September 15, 2014 18:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants