0

The (anti) CSRF Token should protect user from executing a action on the website by clicking a link or a form that is created by an attacker.

In the application that I want to secure I can't use an existing framework and I can't use html forms everywhere and I can't use Javascript to set headers. The application often uses just simple links and this can not easily be changed.

Still I like to have CSRF protection and want to know if the following approach would be secure. The approach relies only on a strict samesite cookie.

It should work like this:

A) For users without session:

When the application is installed a strong random(256bit) secret is generated (called SHARED_SECRET).

When a user visits the page the first time, a the cookie (with settings samesite=strict, secure, http-only, host-only) with the a strong random (128bit called R) + TIMESTAMP + SIGNATURE is created. I call it CSRF_TOKEN. The SIGNATURE is defined as hmac_sha256(R+TIMESTAMP,SHARED_SECRET). Every page load or ajax request creates a new CSRF_TOKEN and replaces the existing CSRF_TOKEN.

If the user does a action e.g. by pressing a action link the CSRF_TOKEN cookie is read on server side. The value of the CSRF_TOKEN cookie is split into R, TIMESTAMP and SIGNATURE and the SIGNATURE' is calculated again using hmac(R+TIMESTAMP,SHARED_SECRET) the result compared against the SIGNATURE from the CSRF_TOKEN.

If the signatures are equal (with a equal function save against timing attacks) and the timestamp is not older then ~15min and Sec-Fetch-Site is not present or value equals same-origin the action will be executed.

B) For users with session:

The session key is stored in cookie with (samesite=lax, secure, http-only, host-only) and a strong random csrf token is generated and stored in the CSRF_TOKEN cookie (samesite=strict,secure, http-only, host-only) and within the server side session store.

For each action triggered by the user the session is loaded and CSRF_TOKEN token is compared with the value stored in the session and if Sec-Fetch-Site is present it's checked if the value equals same-origin.

I guess with the good browser support for the samesite=strict and Sec-Fetch-Site feature it may be a secure solution but maybe I missing something. Would it be secure CSRF protection?

key_
  • 1
  • 5
  • welcome - please clarify whether R contains any timestamp data, and if not, whether it could be modified? – brynk Jun 22 '22 at 17:01
  • it does not, should I add a timestamp? – key_ Jun 22 '22 at 18:13
  • i asked because this (or tieing to the session) would likely contribute to a compromise/ partial solution (if one is possible!? https://security.stackexchange.com/a/225106/ CBHacking'20), eg. under your scheme if i can convince your browser to open a link to "delete account id 0", would a valid cookie be sent? there are many existing Q+A's (eg. https://security.stackexchange.com/a/51229 SilverlightFox'14 describes the prob well) but from what i can tell no recent discussion of stateless operation/ or without modifying the client request to include the token for comparison with the cookie – brynk Jun 22 '22 at 18:59
  • 1
    and more: https://security.stackexchange.com/q/59470/ Gili'14 "Double Submit Cookies vulnerabilities" discussing sub-domains and host-only, setting secure flag, https-only server operation, hsts .. plenty of edge cases to consider (be sure to follow the links in the comments as well) – brynk Jun 22 '22 at 19:14
  • the token ties the session to the form. You need a hidden field in the form for that to work. Make sure the cookie is set "http-only". That bit is important, too... but why would you set same-site to lax? Creating the session and authenticating the session are separate concerns from anti-csrf tokens. These tokens make it harder to impersonate the web site. (the attacker page can no longer be static) – browsermator Jun 22 '22 at 19:21
  • @pcalkins there is no form, in some cases there are simple links. The session cookie is set to lax (for normal users) because they do have more comfort when entering the page from 3rd party websites e.g. from google search result, so they still logged in. I think I do not get the last point. – key_ Jun 22 '22 at 21:03
  • I don't think you should ever be changing any state using GET requests. That sounds like trouble. What I mean by static is an "app" or "site" that impersonates yours can't be fixed. It would need to insert the token into it's DOM in order to send the request on behalf of the user (the cookie would be sent automatically since it's sent to the same domain, but not the hidden field) Though an iframe loaded with the original site might work? – browsermator Jun 22 '22 at 22:06
  • @pcalkins Iframe is forced to samesite by security headers. Anyway as far I know the browser would not automatically send the samesite=strict cookie within a iframe that is hosted on another domain. – key_ Jun 25 '22 at 16:42
  • I found a similar question here https://security.stackexchange.com/questions/201396/samesite-cookie-attribute-and-synchronizer-token-pattern?rq=1 – key_ Jun 26 '22 at 07:19

2 Answers2

2

This approach is overcomplicted and probably unnecessary (in the no-session case), and possibly insecure (in the session case) anyhow because it ultimately rests on SameSite, which is a defense-in-depth measure rather than a reliable protection.

First of all, you talk about CSRF protection for users without a session, but that almost certainly doesn't make sense. The usual purpose of CSRF is that it's a way for an attacker to take actions in the context of the victim's session. Sometimes it matters anyway if you are authorizing users based on something other than session - e.g. by IP address, location within a LAN/VPN/localhost, HTTPS session, HTTP Authorization (Basic or Digest), or so on - but that's extremely rare and nothing you've said suggests that is happening here. Given that, trying to prevent CSRF for sessionless users is like trying to lock a car door that isn't part of a car. There's nothing for it to secure.

Second, your whole cookie-for-sessionless-users thing is literally just re-inventing JWTs (better to just use existing systems) and then sticking this neo-JWT in a samesite cookie (reasonable thing to do with a JWT) and calling it an anti-CSRF token (it's not). For anti-CSRF purposes, this is no stronger than having a completely empty cookie called "NotCSRF" which has the same flags as the one you're using. The value of the cookie doesn't matter, because you don't have anything to compare it to. Since you don't have any way to distinguish one user from another (unless you're storing the R value on the server and in some way associating it with the user, in which case this is just a weird session token after all, albeit possibly an unauthenticated session), an attacker can trivially get their own (valid) cookie and send requests using that from their own machine (no need for CSRF, as above). In the event CSRF matters, SameSite on the "NotCSRF" cookie prevents the attacker from making CSRF requests anyhow (you just request any request without a cookie called that). If the attacker could get past the weaknesses in cookie-based CSRF protection (by planting a cookie on the victim's browser), the neoJWT complicated version still offers no more security than the blank cookie because an attacker can just go get their own (valid) cookie and plant that instead.

Third, you say "the application often uses just simple links and this can not easily be changed", with the implication that these links (resulting in simple GET requests) are state-changing. That's bad, both because it's a violation of the HTTP spec (see section 9 of https://www.ietf.org/rfc/rfc2616.txt), and because it violates the expectations of web client software (e.g. some browsers and other web clients pre-fetch links that a user can see, for reasons such as checking them for malware or making navigation to a pre-fetched link "instant"). If you really can't do without it, and can't attach JS to each link to send a client-controlled value, then SameSite is probably your only viable option for CSRF protection (and the approach you describe for users with a session makes sense), but that's a bad state to be in.

Fourth, SameSite isn't nearly as strong a protection as many people assume. Even leaving aside browsers that don't implement it (RIP IE, but some people still use it or ancient Android versions or so on), the scope of a "site" is far broader than the browser's concept of an Origin (as seen in features like same-origin policy, cross-origin resource sharing, and the Origin header). Origin requires a strict match on protocol, domain, and port. Site ignores protocol and port, and handles domain in an odd way: the "site" for a domain is determined by the first element of the domain that has a suffix on the Public Suffix List. In practice, this means that all subdomains of your site are going to be considered the same site for purposes of the SameSite flag (unless your site is itself a public suffix, as e.g. github.io is), and if an attacker can carry out a subdomain takeover attack (or get the victim to access your site over a different port or protocol), the attacker can set cookies and possibly read existing ones.

Some final notes:

  • There's no need to go out of your way to do constant-time comparisons of HMACs; the computation of an HMAC (or any other cryptographically secure hash) makes iterative timing attacks impossible (the attacker can't vary the input in such a way that there's a single controllable change in the output.
  • For comparing a random value in an anti-CSRF cookie against the same value in server-side state, though, you should use a constant-time check. Alternatively, hash/HMAC the value from the cookie (and store the hash/HMAC in the server-side state) to get the built-in protection against timing attacks and also provide a trivial bit of additional protection in the event of somebody getting read access on your DB.
  • There exists a header called Sec-Fetch-Site, which you can use to tell whether a request was made same-origin, cross-origin but same-site, cross-site, or by the user directly selecting a URL (by typing, opening a bookmark, etc. as opposed to navigating from another page by clicking a link, submitting a form, being navigated by script, etc.). Its even less supported than SameSite (Safari doesn't send it, and on browsers that do you need a more recent version), and opening a link from another app (e.g. a chat or email client) looks like the user entering the URL rather than like the user navigating from somewhere (which wouldn't matter, security-wise, if your GETs were safe like they're supposed to be), but for defense-in-depth it might help some?
CBHacking
  • 48,401
  • 3
  • 90
  • 130
  • the stateless situation is given in some places where people can add items to a list and for the login. The login for is in the header of each page and sometimes cached. Do I really not need some csrf protection for this? e.g. see https://stackoverflow.com/questions/6412813/do-login-forms-need-tokens-against-csrf-attacks it sais otherwise – key_ Jun 26 '22 at 11:24
  • I'm unclear what distinction you're drawing between login being cached vs. creating a session, and it's probably dangerous (caching + authentication is in general a risky combo). As for login CSRF, it's fairly rare for that to be a problem, but on some sites (specifically, ones where a user might not know what info to expect to find, or where the site doesn't make it clear when your session changes) it matters. In those cases, though, the standard solution is to create a session - just, an anonymous one - as alluded to in the paragraph starting "Second". – CBHacking Jun 27 '22 at 00:39
  • Also, login absolutely needs to be POST - or at least not GET - and you can presumably put traditional anti-CSRF protections in place there. Of course, they still need a session to anchor on, so you will still need some sort of anonymous session. – CBHacking Jun 27 '22 at 00:41
  • If you use an anonymous session, make sure to start a new session once authenticated. (session-fixation attack: https://owasp.org/www-community/attacks/Session_fixation ) – browsermator Jun 27 '22 at 21:49
  • @CBHacking thank you, please understand I am limited in what am allowed to changed in that application because I have to fix things using plugins. The login form can use POST and csrf token but on the initial request I am not able to create a pre-session because login form is included on pages that are cached in proxies and login form in on every page as part of the header. – key_ Jun 28 '22 at 03:56
  • @CBHacking, would you kindly explain why we don't have to time-safe compare both HMAC values? – Advena Apr 15 '23 at 13:30
  • 1
    @Tanckom An HMAC - like the output of any secure hash function - can't be iteratively brute-forced. If you find an input that takes a little longer to reject (meaning the HMAC is a partial match), that's completely useless information because any change you make to any single bit anywhere in the input will completely change the HMAC. Even if by completely absurd luck you managed to get all but the last byte of the HMAC to collide, you'd still have to brute-force the entire HMAC to get every single byte to match. Thus, timing attacks just aren't viable; there's no way to detect progress. – CBHacking Apr 15 '23 at 19:11
  • I'm not really following... . Isn't a timing attack when a malicious player wants to guess the output of a hash one by one. E.g. hash === 'a', hash === 'b',... and the one that takes longest to resolve is a match. So, what do you mean by "completely useless information because any change you make to any single bit anywhere in the input will completely change the HMAC."? We're not generating new HMAC's, but guessing its values one by one?

    Regarding the brute force, I can only see WAF/rate-limits preventing this partially. With a distributed attack, you'd still be able to pull it off.

    – Advena Apr 17 '23 at 12:49
  • 1
    Timing attacks are a generic class, but in the context of string/buffer comparison, you're basically right. However, in this case the attacker can't control a, b, etc.! The value that the attacker inputs is being hashed (HMACed, but that's just a keyed hash) before comparison, so the attacker can't choose, or even control, what value is being compared. Imagine the process of trying. 1 in 256 inputs will hash such that the first byte matches, great. But you can't iterate on that, locking the matched byte in place and trying the next one, you have to brute force the whole thing at once. – CBHacking Apr 18 '23 at 04:52
  • Oh, I believe I understand now (please correct me if I'm wrong). It's the question's specific example, of a wrongly implemented CSRF token, where a timing-safe comparison would make no difference. However, a right implemented CSRF token, for example a Session-bound & Signed Double Submit Cookie Pattern, requires a timing-safe comparison as attackers otherwise can forge valid HMAC hashes with their chosen session in the message? – Advena Apr 21 '23 at 16:40
  • This SO answer would explain the steps. Timing safe comparison prevents an attacker from forging a valid CSRF Token with a session value of the attacker's choice. However, this is only true for a correctly implemented CSRF Token pattern and not the "made-up" pattern from the original question. (And thanks for your responses @CBHacking, you are very kind!) – Advena Apr 21 '23 at 16:43
  • 1
    If the data to be MACed is static, and the [H]MAC is attacker-provided, then a timing attack is possible (unless the owner re-hashes both the computed and provided MACs before comparison, or uses a constant-time comparison). In the case where the data to be MACed is attacker-provided and the MAC is static, timing-aided brute-force is impossible. In cases where both are provided by the attacker - which is true for JWTs and similar; I may have been a little hasty to dismiss this - the attacker could hold the data constant and vary only the MAC, trying to iteratively brute-force it. – CBHacking Apr 22 '23 at 02:25
  • Imagining we have a payload of <MESSAGE>-<MAC> which is validated server side. From your first statement, I guess an example attack would be sending requests like: a-> '{"send_to": "hacker@b.com","amount": 1000}-aecf5388e...' ; b -> '{"send_to": "hacker@b.com","amount": 1000}-becf5388e...' ; c -> '{"send_to": "hacker@b.com","amount": 1000}-cecf5388e...' ; ... until all 256 inputs have been tried. – Advena Apr 22 '23 at 13:05
  • From your second statement with "timing-aided brute-force is impossible", it sounds like an example could be: '{"send_to": "hacker0@b.com","amount": 1000}-63f242...'; '{"send_to": "hacker1@b.com","amount": 1000}-63f242...'; '{"send_to": "hacker2@b.com","amount": 1000}-63f242...'? What would even be the point of trying such an attack? – Advena Apr 22 '23 at 13:06
  • And about the last statement, I understand that a JWT is a base64 encoded message with its MAC. I haven't looked into how JWT validation is done, but would imagine that a constant-time comparison is used(??). That said, does that mean that the last statement uses the same attack pattern as in the first example? Why is this considered a brute-force attack compared to the first statement, which is a timing attack? – Advena Apr 29 '23 at 15:27
0

You can not stick only with samesite, it has its own limitations and overall we can not rely on this cookie flag for complete mitigation for CSRF Vulnerability. You have to use anti-CSRF Tokens.

You can create anti-CSRF tokens and places in these areas -

  • Customer Request header - such as - X-CSRF-Token (Best). But Not in cookie header
  • POST request parameters.
  • GET parameters

PLEASE NOTE: Validation on server-side for the token is a MUST

Sir Muffington
  • 1,611
  • 2
  • 13
  • 25
  • thank, I will do so, but token in GET parameter is not ok because it could be exposed during screenshare and within log files, so I will stick to POST and somehow think about to change all the links into form and rewite existing scripts to aet the header. – key_ Jun 29 '22 at 13:58