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

CSRF | Remove deprecated csurf libary and replace with simple implementation #3010

Merged
merged 2 commits into from
Jan 6, 2025

Conversation

coldlink
Copy link
Member

@coldlink coldlink commented Dec 17, 2024

What does this change?

The CSRF libary we were using in Gateway has long been deprecated (archived Sep 14, 2022, last published Jan 19, 2020): https://github.com/expressjs/csurf

Thankfully the risk was fairly minor as the protection still works, there just won't be any security updates to it. Their recommendation was to search npm for an alternate package should we need one: https://www.npmjs.com/search?q=express%20csrf

However going through the list of packages, there is no library which adequately replaces the same package. The closest was https://github.com/Psifi-Solutions/csrf-csrf, however that still had too many options that we didn't need.

Instead we decided to create our own custom implementation based on the OWASP Cheat sheet recommended pattern: Signed Double-Submit Cookie. We also used https://github.com/Psifi-Solutions/csrf-csrf as a reference too.

We made a similar API that the csurf library provided, so we don't have to make too much changes elsewhere in the Gateway project.

Implementation

For generating the hash:

createHmac("SHA256", secret, token)
encode to base64url
return hash

For generating the CSRF token:

generate a new CSRF token using a cryptographically secure random number generator
encode token to base64url
generate a hash of the token using HMAC and a secret key
encode hash to base64url
set a cookie with the token and hash
return the token

For validating the request:

flowchart TD
    start[Request] --> method{Check Method}
    method -->|is ignoredMethod<br>default to GET/OPTIONS| continue[continue with<br>request]
    method -->|not ignoredMethod| route{Check Route}
    route -->|is ignoredRoute<br>no defaults| continue
    route -->|not ignoredRoute| validate(Validate CSRF<br>on Request)
    validate -->|get csrf cookie| check-cookie{Cookie exists?}
    check-cookie -->|No| error[throw invalid<br>csrf error]
    check-cookie -->|Yes| split-cookie(Split cookie value<br>by delimiter<br>into token and hash)
    split-cookie --> split-cookie-check{Token/Hash exists<br>and are strings?}
    split-cookie-check -->|No| error
    split-cookie-check -->|Yes| token-body(Get token from<br>request body)
    token-body --> token-body-check{Token from body<br>exists and string?}
    token-body-check -->|No| error
    token-body-check -->|Yes| compare-token{Compare token from<br>cookie to body}
    compare-token -->|not same| error
    compare-token -->|same| compare-hash{Compare hash<br>from cookie<br>to generated hash<br>from token}
    compare-hash -->|not same| error
    compare-hash --> |same| continue
Loading

Unlike the OWASP example, we don't have a session to bind the CSRF token to, which would lead to the "Naive Double Submit Cookie" pattern, with security implications mentioned in https://owasp.org/www-chapter-london/assets/slides/David_Johansson-Double_Defeat_of_Double-Submit_Cookie.pdf

However we can avoid this pitfall by using signed cookies, which are less vulnerable to the same attacks, as they are signed with a (different) secret key, meaning that the attacker cannot forge a valid CSRF token, as any changes to the token would invalidate the signature, and the server wouldn't make the cookie available.

We also use the __Host- prefix to the cookie name where if a cookie name has this prefix, it's accepted in a Set-Cookie header only if it's also marked with the Secure attribute, was sent from a secure origin, does not include a Domain attribute, and has the Path attribute set to /. In other words, the cookie is domain-locked. Also see https://archive.is/8hPYX

We also only support supplying the token in the request via req.body, using a header or query parameters is not supported, as we don't use this in Gateway.

Tested

  • Unit tests
  • DEV
  • CODE

Notes

This could potentially be refactored out into it's own library in the future, especially if there's other projects at the Guardian (or elsewhere) that could benefit from this!

@coldlink coldlink linked an issue Dec 17, 2024 that may be closed by this pull request
@coldlink coldlink force-pushed the mm/migrate-csrf branch 2 times, most recently from f51fa35 to 6f4e654 Compare December 17, 2024 16:18
@coldlink coldlink force-pushed the mm/migrate-csrf branch 2 times, most recently from e8afadc to a5f5693 Compare December 18, 2024 09:50
@coldlink coldlink marked this pull request as ready for review December 18, 2024 11:00
@coldlink coldlink requested a review from a team as a code owner December 18, 2024 11:00
@coldlink coldlink requested review from a team and removed request for a team December 18, 2024 12:00
@coldlink coldlink requested review from guardian-ci and removed request for guardian-ci January 6, 2025 08:06
@coldlink coldlink merged commit bd0ea21 into main Jan 6, 2025
21 checks passed
@coldlink coldlink deleted the mm/migrate-csrf branch January 6, 2025 08:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

csurf deprecation
2 participants