Skip to content

Prevent accidental leak of PII when Copy & Pasting of Flow URLs which include Flow IDs #1282

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
aeneasr opened this issue Apr 27, 2021 · 14 comments · Fixed by #1458
Closed
Assignees
Labels
feat New feature or request. rfc A request for comments to discuss and share ideas.

Comments

@aeneasr
Copy link
Member

aeneasr commented Apr 27, 2021

Is your feature request related to a problem? Please describe.

While UUIDs are hard to guess, they are very prominent in the URL:

https://kratos-ui/registration?flow=1234...

This might tempt users to copy and paste that link and send it to someone else (Alice says: "Hey Bob, here's the registration URL!"). There are two problems with that:

  1. Bob will not be able to complete the flow because he does not have the CSRF cookie from Alice
  2. If Alice submitted the form but a validation error occurred (e.g. "website can not be empty"), then Bob will see (partially) what Alice entered in the form. While we strip some data (e.g. password), this can still leak PII to others.

Describe the solution you'd like

Before Ory Kratos 0.5 we had a CSRF protection in place for public endpoint. Fetching the flow would require sending the CSRF cookie as well. This completely disarms the second example as Bob does not have Alice's CSRF Cookie. This can also allow Ory Kratos to instruct the UI to re-initialize the flow. However, this only works for requests made to the Public API - hence single-page apps / browser apps.

This becomes problematic for the Admin APIs used by server-side apps (e.g. NodeJS). Here, the server-side ui (e.g. our NodeJS example) makes a HTTP request to the Admin API to fetch the data. Requiring a CSRF cookie on Kratos' Admin API will not work here because the UI might be hosted on another domain and might not have access to the CSRF cookie of Ory Kratos.

Thus, we need another mechanism to prevent the NodeJS app of leaking the information. There are several possibilities to achieve this:

  1. The Admin API allows a flow to be fetched exactly once. To reset this counter, the user has to be redirected to a URL which checks if the CSRF cookie is set and thus reset the counter. This is probably the safest approach as the flows are bound to a specific browser session.
  2. The Admin API requires meta-data from the original browser request to create and compare the browser's fingerprint. Such information could include:
  • Client IP
  • User Agent
  • Accept
  • Accept-Encoding
  • Accept-Language

Both approaches leave a little bit of attack surface:

  1. Malice could trick Alice into opening the refresh endpoint and trying to be faster than Alice to fetch the flow information. This would imply that:
    1. Alice manually gave Malice the URL with the flow ID (e.g. 1234)
    2. Malice could convince Alice to call the refresh endpoint for flow 1234
    3. Flow 1234 has not expired yet (usually expiry time is between 15 and 30 minutes)
    4. Malice intercepts the redirect, or is simply faster than the redirect, back to the server-side UI which is making the call to the Admin Flow API. This can be made harder by introducing some rate limiting (e.g. max 1 request per second to the Admin Flow API per Flow ID).
  2. Alice and Malice have identical devices with identical operating systems and browser versions and are on the same network and Alice would share the Flow ID with Malice.

Describe alternatives you've considered

Requiring the CSRF cookie to be available on the domain that hosts the UI on the server side.

Additional context

Guessing the UUID is not really a problem because registration, login, ... flows have relatively short expiry times and use UUIDs. To find a collision here is not possible as the subset of issued UUIDs will never go above one billion.

Thus, the probability to find a duplicate within 103 trillion version-4 UUIDs is one in a billion.

Even with 100 trillion - making one billion network requests takes a lot of time!

So, what do you think is the best approach here?

@aeneasr aeneasr added feat New feature or request. rfc A request for comments to discuss and share ideas. labels Apr 27, 2021
@aeneasr aeneasr added this to the v0.7.0-alpha.1 milestone Apr 27, 2021
@aeneasr aeneasr self-assigned this Apr 27, 2021
@aeneasr aeneasr changed the title Reintroduce CSRF protection for flow APIs Prevent accidental leak of PII when Copy & Pasting of Flow URLs which include Flow IDs Apr 27, 2021
@abador
Copy link
Contributor

abador commented Apr 27, 2021

Both solutions seem acceptable.
Some remarks:

  1. Seems pretty straight forward. Even if you use a proxy you probably take care of forwarding the cookies so checking them shouldn't add much complexity to the overall setup. I would only suggest to set the number of requests before invalidation based on the config just to cover some specific setups where it might be needed
  2. I can see many problems with this if we add proxies/lbs before the app. In case we don't forward some or all the headers this won't work or will be unstable. Sometimes only minimal headers are forwarded. What if at some point there is a need to add some additional headers for the fingerprint to work? This may impact the overall update cycle in some cases

I would go with 1

@zepatrik
Copy link
Member

One other approach I see is not having the session ID in the URL in the first place. Having it in a cookie instead will eliminate the risk of passing on the URL completely.

@dduzgun-security
Copy link

I would go with the first option as well.

Tracking the user's browser fingerprint (IP, location, a unique DeviceID cookie that is generated and stored on the browser, User-Agent, ...) can be useful to notify that a new device is requesting access. Having the option to do extra flows on new device detection could be nice. (Regenerating the FlowID if the DeviceID changed, notifying the user that a new device tries to access his/her account, enforcing MFA on the first login with a new device, ...) What do you think?

@christian-roggia
Copy link

I fully quote @zepatrik solution, having the flow id in the URL makes it a bit "weird" to the end-users as this is not common for website logins (at least the most popular ones). Additionally, as highlighted in this ticket, users tend to copy/paste the full URL disregarding any leak of potentially sensitive information. I did so myself by mistake more than once.

@aeneasr
Copy link
Member Author

aeneasr commented Apr 27, 2021

One other approach I see is not having the session ID in the URL in the first place. Having it in a cookie instead will eliminate the risk of passing on the URL completely.

I agree, but how would this work IRL? I see several problems:

  1. Ory Kratos might not have control (foo.kratos.com) over the UI's (myui.com) domain meaning it can not set the cookie
  2. The UI would need to decode the cookie or forward it to Ory Kratos

Alternatively we could ask the UIs to do a 302 with Set-Cookie when ?flow=... is in the URL to the same URL without the flow ID. It then gets read from the cookie. However, implementing this sucks and is not very easy.

What other alternatives do we have?

@aeneasr
Copy link
Member Author

aeneasr commented Apr 27, 2021

One possibility could be to do the redirects using 307 which would allow Ory Kratos to forward the request (which includes the flow id in the post body) to the UI. The UI would then parse the body and extract the flow id.

However, this does not work for the initial request because that is always a GET request <a href="https://kratos/self-service/login/browser> meaning we can't make that into a POST redirect. If we were to do this using <form method="post" action="https://kratos/self-service/login><button value="login"></form> we still have the problem that we can not inject the flow ID, as we can only forward the SAME payload (so value=login).

Generally, this would also not work with OIDC flows as we end up with a GET callback meaning we can't upgrade that to a POST request.

@abador
Copy link
Contributor

abador commented Apr 28, 2021

In my opinion doing post redirects won't work in the long run. The browser behaviour was changed more than once in that regard and there is also the fact that some browsers have some additional security rules that will just make that impossible. The main focus should be stability on different devices since people tend to do weird things when something doesn't work (like copying links to forums)

Removing the flow param would be nice, but to limit the complexity we would need to rethink the browser flow a bit.

@aeneasr
Copy link
Member Author

aeneasr commented Apr 28, 2021

In my opinion doing post redirects won't work in the long run.

I think POST redirects won't go away, actually 307 and 308 were introduced to deprecate 302 (which is deprecated but kept indefinitely due to popularity)! What I worry more about is cookies in redirects. This is already pretty difficult today and will only get harder in the future.

Maybe we just need to require for Ory Kratos and the login UI to be on the same domain?

@zepatrik
Copy link
Member

I agree, but how would this work IRL?

I guess it has the same challenges as your proposed second solution. Cookies are in essence just metadata, and it might be easier to forward them as they are more standardized than the other metadata you proposed.
Maybe it will be easier to use a custom header that is used like a cookie. That can be accessed by the client JS and is supported by all kinds of proxies and frameworks.

@aeneasr
Copy link
Member Author

aeneasr commented Apr 28, 2021

As long as everything is on one TLD, cookies would be the best option. For non-TLD set ups we might need to add a proxy (thinking about you, ory proxy...)

@aeneasr
Copy link
Member Author

aeneasr commented Apr 28, 2021

I guess it has the same challenges as your proposed second solution. Cookies are in essence just metadata, and it might be easier to forward them as they are more standardized than the other metadata you proposed.

Well, not really - IP and User Agent are much easier to collect than Cookies which are seeing an intense crackdown in the browser world, following SameSite security measuers and other Do Not Track technology! But if we operate under one TLD everything should be fine!

@mitar
Copy link
Contributor

mitar commented May 12, 2021

We had the same discussion in our oidc solution (built on top of fosite) and we decided that we would use both the flow ID and a session cookie. So only when both are present, state is valid. We want flow ID in the URL so that user might not get confused by doing inside multiple tabs, e.g., if in one tab user is approving app A and in another tab user is approving app B. So we want to keep that separate but we also want flows to be valid only in this browser. So, a session cookie is set.

@aeneasr
Copy link
Member Author

aeneasr commented Jun 15, 2021

The only truly secure option here is probably to initialize the flow not using the browser but instead using the backend. The Ory Kratos API then sends something like:

{
  set_cookie: "..."
  flow: {
    // ...
  } 
}

which then is used by the backend (so no initial redirection is required!) to render the form as usual:

<form action="https://kratos/self-service/registration">
  <...>

The submission could either be against the backend (a bit more flexible but difficult for CSRF), or directly against ory kratos (a bit easier but not as flexible). In the latter case, and if a validation error occurred, ory kratos would redirect to the registration ui and include the flow ID as we know. However, for the backend to make a successful request, it would need to look like this:

POST https://kratos/self-service/registration/flows/1234

{
  "cookie_headers": ["...", "..."]
}

// OR

GET https://kratos/self-service/registration/flows/1234
Cookie: ...
Cookie: ...
Cookie: ...

This would be a pretty significant API change. As an alternative we could use heuristics (e.g. IP address) to keep the flow as-is.


Another alternative would be to keep the flow as is and require the UI and Ory Kratos to be co-located on the same domain, so that Ory Kratos can set the CSRF cookie for all of the domain, and the UI then uses not the administrative endpoint but instead the public endpoint and is required to include the CSRF cookie in the request.

That would certainly be easier to implement on Ory Kratos' side, have less breaking changes, and - if documented correctly and the SDK is playing along - could be pretty straight forward to implement.

@aeneasr
Copy link
Member Author

aeneasr commented Jun 16, 2021

I think the last idea is probably the best one. Will pursuit that option!

aeneasr added a commit that referenced this issue Jun 23, 2021
aeneasr added a commit that referenced this issue Jun 30, 2021
BREAKING CHANGE: This patch introduces CSRF countermeasures for fetching all self-service flows. This ensures that users can not accidentally leak sensitive information when copy/pasting e.g. login URLs (see #1282). If a self-service flow for browsers is requested, the CSRF cookie must be included in the call, regardless if it is a client-side browser app or a server-side browser app calling. This **does not apply** for API-based flows.

As part of this change, the following endpoints have been removed:

- `GET <ory-kratos-admin>/self-service/login/flows`;
- `GET <ory-kratos-admin>/self-service/registration/flows`;
- `GET <ory-kratos-admin>/self-service/verification/flows`;
- `GET <ory-kratos-admin>/self-service/recovery/flows`;
- `GET <ory-kratos-admin>/self-service/settings/flows`.

Please ensure that your server-side applications use the public port (e.g. `GET <ory-kratos-public>/self-service/login/flows`) for fetching self-service flows going forward.

If you use the SDKs, upgrading is easy by adding the `cookie` header when fetching the flows. This is only required when **using browser flows on the server side**.

The following example illustrates a ExpressJS (NodeJS) server-side application fetching the self-service flows.

```patch
app.get('some-route', (req: Request, res: Response) => {
-   kratos.getSelfServiceLoginFlow(flow).then((flow) => /* ... */ )
+   kratos.getSelfServiceLoginFlow(flow, req.header('cookie')).then((flow) => /* ... */ )

-   kratos.getSelfServiceRecoveryFlow(flow).then((flow) => /* ... */ )
+   kratos.getSelfServiceRecoveryFlow(flow, req.header('cookie')).then((flow) => /* ... */ )

-   kratos.getSelfServiceRegistrationFlow(flow).then((flow) => /* ... */ )
+   kratos.getSelfServiceRegistrationFlow(flow, req.header('cookie')).then((flow) => /* ... */ )

-   kratos.getSelfServiceVerificationFlow(flow).then((flow) => /* ... */ )
+   kratos.getSelfServiceVerificationFlow(flow, req.header('cookie')).then((flow) => /* ... */ )

-   kratos.getSelfServiceSettingsFlow(flow).then((flow) => /* ... */ )
+   kratos.getSelfServiceSettingsFlow(flow, undefined, req.header('cookie')).then((flow) => /* ... */ )
})
```

For concrete details, check out [the changes in the NodeJS app](ory/kratos-selfservice-ui-node@e7fa292).

Closes #1282
t-tomalak pushed a commit to Wikia/kratos that referenced this issue Jul 16, 2021
About two months ago we released Ory Kratos v0.6. Today, we are excited to announce the next iteration of Ory Kratos v0.7! This release includes 215 commits from 24 contributors with over 770 files and more than 100.000 lines of code changed!

Ory Kratos v0.7 brings massive developer experience improvements:

- A reworked, tested, and standardized SDK based on OpenAPI 3.0.3 ([ory#1477](ory#1477), [ory#1424](ory#1424));
- Native support of Single-Page-Apps (ReactJS, AngularJS, ...) for all self-service flows ([ory#1367](ory#1367));
- Sign in with Yandex, VK, Auth0, Slack;
- An all-new, secure logout flow ([ory#1433](ory#1433));
- Important security updates to the self-service GET APIs ([ory#1458](ory#1458), [ory#1282](ory#1282));
- Built-in support for TLS ([ory#1466](ory#1466));
- Improved documentation and Go Module structure;
- Resolving a case-sensitivity bug in self-service recovery and verification flows;
- Improved performance for listing identities;
- Support for Instant tracing ([ory#1429](ory#1429));
- Improved control for SMTPS, supporting SSL and STARTTLS ([ory#1430](ory#1430));
- Ability to run Ory Kratos in networks without outbound requests ([ory#1445](ory#1445));
- Improved control over HTTP Cookie behavior ([ory#1531](ory#1531));
- Several smaller user experience improvements and bug fixes;
- Improved e2e test pipeline.

In the next iteration of Ory Kratos, we will focus on providing a NextJS example application for the SPA integration as well as the long-awaited MFA flows!

Please be aware that upgrading to Ory Kratos 0.7 requires you to apply SQL migrations. Make sure to back up your database before migration!

For more details on breaking changes and patch notes, see below.
jess-sheneberger pushed a commit to jess-sheneberger/kratos that referenced this issue Jul 21, 2021
About two months ago we released Ory Kratos v0.6. Today, we are excited to announce the next iteration of Ory Kratos v0.7! This release includes 215 commits from 24 contributors with over 770 files and more than 100.000 lines of code changed!

Ory Kratos v0.7 brings massive developer experience improvements:

- A reworked, tested, and standardized SDK based on OpenAPI 3.0.3 ([ory#1477](ory#1477), [ory#1424](ory#1424));
- Native support of Single-Page-Apps (ReactJS, AngularJS, ...) for all self-service flows ([ory#1367](ory#1367));
- Sign in with Yandex, VK, Auth0, Slack;
- An all-new, secure logout flow ([ory#1433](ory#1433));
- Important security updates to the self-service GET APIs ([ory#1458](ory#1458), [ory#1282](ory#1282));
- Built-in support for TLS ([ory#1466](ory#1466));
- Improved documentation and Go Module structure;
- Resolving a case-sensitivity bug in self-service recovery and verification flows;
- Improved performance for listing identities;
- Support for Instant tracing ([ory#1429](ory#1429));
- Improved control for SMTPS, supporting SSL and STARTTLS ([ory#1430](ory#1430));
- Ability to run Ory Kratos in networks without outbound requests ([ory#1445](ory#1445));
- Improved control over HTTP Cookie behavior ([ory#1531](ory#1531));
- Several smaller user experience improvements and bug fixes;
- Improved e2e test pipeline.

In the next iteration of Ory Kratos, we will focus on providing a NextJS example application for the SPA integration as well as the long-awaited MFA flows!

Please be aware that upgrading to Ory Kratos 0.7 requires you to apply SQL migrations. Make sure to back up your database before migration!

For more details on breaking changes and patch notes, see below.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feat New feature or request. rfc A request for comments to discuss and share ideas.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants