Skip to content
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

this library is "insecure" to CSRF by default, security mechanisms confusing to use and enable #164

Open
joshribakoff-sm opened this issue Aug 9, 2022 · 8 comments
Labels
documentation Improvements or additions to documentation

Comments

@joshribakoff-sm
Copy link

joshribakoff-sm commented Aug 9, 2022

Our security team flagged that we were missing CSRF protection, which was surprising because we chose a open source library that is widely used, assuming that it would be secure by default if it was so widely used.

In #138 support for state param was added. It states that you can pass true to enable CSRF protection; however my team found that even after setting it to true, the library did not append any state pram to the redirect URL.

Finally we were able to store a token into our session manually and then pass it as the state param to passport.authenticate()'s 2nd argument. This isn't even documented in the README for the project.

There is also this issue that is related from five years ago that has gone unaddressed where another user flagged that the library is insecure [by default] and could not figure out how to enable the security. #74

Is there any reason to not have CSRF protection either on by default, or always on? At the very least there should be more prominent warnings logged to stderr or in your a big warning in your README so that people know that they are not running a production ready set up when they choose this library, without further efforts.

An example of an "attack" is that an attacker can stop their own login flow and send a URL with their code to a victim, upon clicking the link the victim is logged in as the attacker. This is a security problem because the victim could unintentionally pay the attacker's bill, or could enter their social security number into a form that gets saved to the account that the attacker controls, etc..

It is also required to have CSRF protection on both the client and the server, according to the Oauth2 spec

@joshribakoff-sm
Copy link
Author

According to #117 (comment) this library is also abandoned...

@jaredhanson
Copy link
Owner

jaredhanson commented Aug 9, 2022

You are covering a lot of topics in this issue, and there's a lot of incorrect statements.

To start with, this library is not abandoned.

Next, you say that state support was added in #138, which is not entirely accurate. This library has long supported (I can't even remember how long) state by enabling it in the constructor. You can see a working configuration here:

passport.use(new GoogleStrategy({
  clientID: process.env['GOOGLE_CLIENT_ID'],
  clientSecret: process.env['GOOGLE_CLIENT_SECRET'],
  callbackURL: '/oauth2/redirect/google',
  scope: [ 'profile' ],
  state: true
},

#138 added more enhancements that allowed controlling of state on a per-route basis, rather than globally in the constructor, as described here.

I can assure you this functionality works and is comprehensively covered by unit tests. If it is not working in your application, it may be misconfigured or used incorrectly, but there's not enough detail or code samples provided here to make that determination. It would help to understand which OAuth strategies and versions you are using to make a determination.

Finally, state is recommended in the OAuth 2.0 spec, but not required, per section 4.1.1. For historical reasons, this library did not require it (since the spec itself did not require it). It would probably be a better to enable it by default, but that would cause potential backwards incompatibilities. I agree that there should be more warnings when it is not enabled, so will take the feedback to add them in an upcoming release.

@joshribakoff-sm
Copy link
Author

joshribakoff-sm commented Aug 10, 2022

Unit tests can sometimes not match up with e2e tests.

I'm not alleging you abandoned your library [merely linking to allegations others have made, which went uncontested], nor am I alleging you intended a security lapse intentionally, but I did want to make sure my issue was strongly worded to catch people's attention, so thank you for the quick reply.

I can work on a minimal reproduction, but I am confident on our end we are not doing anything to disable the CSRF protection, and what we observe is that the outbound redirect has no state, and upon redirecting back after authentication there is also no state param passed.

The only way we got it to work was by passing state manually, and we found it was also necessary to pass a string instead of true, and roll our own validation.

This library has long supported (I can't even remember how long) state by enabling it in the constructor.

My apologies for not being familiar with the history of this library. If it works, that is still opt in, and according to https://www.passportjs.org/tutorials/google/state/ it is supposed to happen by default within Passport:

For security, state needs to be maintained between these two redirects. Passport does this automatically, but the app first needs session support.

But this is not what we observed, nor do I see any code to handle state params in passport itself. So that was pretty confusing. Secondly, even after we set state to true, my team observed that the library still did not add any state param to the outbound redirect (I didn't personally verify this), and my team found that it was necessary to pass a string to have any observable effect.

It would probably be a better to enable it by default, but that would cause potential backwards incompatibilities. I agree that there should be more warnings when it is not enabled, so will take the feedback to add them in an upcoming release.

Awesome, thanks! Yeah adding a warning to the current major version, and perhaps a new major version where it is enabled by default would be warranted in my opinion.

Finally, state is recommended in the OAuth 2.0 spec, but not required, per section 4.1.1. For historical reasons, this library did not require it (since the spec itself did not require it).

CSRF protection is required, and this library defaults to no CSRF protection (via state or otherwise)

@joshribakoff-sm
Copy link
Author

my team observed that the library still did not add any state param to the outbound redirect (I didn't personally verify this)

Actually I just personally verified this on our end. It's also a type error in Typescript, see https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/passport/index.d.ts#L67

I then noticed definitely typed has separate AuthenticateOptions for passport-oauth2 specifically where state is typed as any.

In our code, we import passport, we add local and oauth2 strategies, then we pass this passport with the strategies added to our routes, and in our route we call authenticate() on it like this:

passport.authenticate('oauth2', ... )

Perhaps there is an authenticate on the passport itself, and on the strategy itself. Perhaps your unit tests verify the latter, but perhaps real world usage is to use the former?

@jaredhanson
Copy link
Owner

jaredhanson commented Aug 10, 2022

My apologies for not being familiar with the history of this library. If it works, that is still opt in, and according to https://www.passportjs.org/tutorials/google/state/ it is supposed to happen by default within Passport:

The tutorial you link to is using passport-google-oidc, which internally uses passport-openidconnect. It does not use passport-oauth2, so the state handling is slightly different (mainly, its default to on, as documented).

I am unable to reproduce the issue you are filing. You should also be able to confirm the intended operation (primarily, that state is included when enabled) by running the sample application here: https://github.com/passport/todos-express-google-oauth2

From the information you have provided, it seems that the most likely cause is how Passport and this strategy is being integrated into your application. In order to provide any further help, can you provide exact code of how you are initializing strategies, and how you are using them in routes? It would also help to know which OAuth 2 strategies and versions you are using.

@jaredhanson jaredhanson added the question Request for help or support label Aug 10, 2022
@joshribakoff-sm
Copy link
Author

joshribakoff-sm commented Aug 10, 2022

re: the 'question' label, I don't have any question here.

The tutorial you link does not use passport-oauth2

The feedback I am giving is that as a user it added confusion, since this [unrelated] lib is publishing misleading or incorrect information in their docs about Passport itself. It may not be directly under your control, however my feedback is that you could mitigate this by backfilling the missing documentation [for your strategy] in this repo's readme file.

I am unable to reproduce the issue you are filing

As mentioned above that's because you're passing state: true to https://github.com/passport/todos-express-google-oauth2/blob/master/routes/auth.js#L19 and not here https://github.com/passport/todos-express-google-oauth2/blob/master/routes/auth.js#L114, which is the footgun I am calling out.

Perhaps there is an authenticate on the passport itself, and on the strategy itself. Perhaps your unit tests verify the latter, but perhaps real world usage is to use the former?

As the author of your lib, I understand you are not as susceptible to this footgun, but try to imagine as a new user (would it be obvious?). I can say for myself, it was not obvious, and others have raised this feedback that they could not figure out why CSRF is not working either.

From the information you have provided, it seems that the most likely cause is how Passport and this strategy is being integrated into your application

No, I don't have any issue at this time. I got CSRF protection working in my app prior to opening this issue. The purpose of my issue report is to deliver feedback that you are defaulting to an insecure setup, and there is a lack of docs [thereby making it unnecessarily confusing for users to integrate it properly]. I am clear on how to integrate it, in hindsight.

In summary, there are 3 pieces of feedback from me:

  • suggestion to backfill missing docs about how a user can turn on/off the CSRF protection with the state param
  • suggestion to comply with spec by default by having CSRF security default to "on", see the spec section 10.12 stating that " The client MUST implement CSRF protection "
  • logging the warnings when CSRF is disabled (at least for older versions where it does not comply with the spec by default)

It would also help to know which OAuth 2 strategies and versions you are using.

My issue is filed on the repo corresponding to the strategy I am using, there is no CSRF protection by default [even on latest version], and there are no docs on how to enable it in your current readme on master branch.

FYI I am unsubscribing from this thread because I feel like I have made my feedback clear, I don't need any help, and feel like I am allowing myself to come across as abrasive, which is not intentional. I do appreciate this library, and certainly would hope my feedback can be constructively received to make the library even better

@jaredhanson
Copy link
Owner

It would help me if you could clearly articulate if you are reporting an issue, or just struggling with integrating this package into your application.

In the filing of this issue, you are clearly indicating that you don't believe the package is correctly adding state parameters. I don't believe that is true, but if it is I want to fix it.

If you want to pass a state option to passport.authenticate(), the following should work as described here:

  1. Initialize the OAuth 2-based strategy with a store: true option:
var GoogleStrategy = require('passport-google-oauth20');

passport.use(new GoogleStrategy({
    clientID: process.env['GOOGLE_CLIENT_ID'],
    clientSecret: process.env['GOOGLE_CLIENT_SECRET'],
    callbackURL: '/auth/google/callback',
    scope: 'profile',
    store: true
  },
  function(accessToken, refreshToken, profile, cb) {
    // ...
  }
));
  1. Pass state as an object to passport.authenticate()
passport.authenticate('google', { state: { beep: 'boop' } })

If you do not have any state to stare, omit the state option, and a state parameter should be auto-generated and included in the redirect.

I understand you found a different solution, but it is relying on undocumented behavior. I would like to confirm that the intended behavior is correct and functional. Can you confirm this?

@jaredhanson jaredhanson added documentation Improvements or additions to documentation and removed question Request for help or support labels Aug 10, 2022
@sghoe
Copy link

sghoe commented Dec 8, 2022

Hello @jaredhanson
I have a question about this state topic. If i understand correct the state will be stored in the session.store like for example a MongoStore.

Is this state stored the hole time?

After login i can see that in the db:
{ "_id": "eVEVsQTCC3gZ0DiaTHVs00BBTFKDuT-Y", "expires": { "$date": { "$numberLong": "1670578477518" } }, "session": "{\"cookie\":{\"originalMaxAge\":null,\"expires\":null,\"httpOnly\":true,\"path\":\"/\"},\"passport\":{\"user\":{\"name\":\"Testi Testmann\",\"username\":\"Testi\"}}}" }

I am using Node 18 with
"passport": "0.4.0",
"passport-oauth2": "1.6.1"

Thank you for the passport libs. Working with them for years now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

3 participants