How to protect against CSRF on a static site?
Asked Answered
S

4

16

I have a static website, being served from a CDN, that communicates with an API via AJAX. How do I protect against CSRF?

Since I do not have control over how the static website is served, I cannot generate a CSRF token when someone loads my static website (and insert the token into forms or send it with my AJAX requests). I could create a GET endpoint to retrieve the token, but it seems like an attacker could simply access that endpoint and use the token it provides?

Is there an effective way to prevent against CSRF with this stack?


Additional details: authentication is completely separate here. Some of the API requests for which I want CSRF protection are authenticated endpoints, and some are public POST requests (but I want to confirm that they are coming from my site, not someone else's)

Skylab answered 28/6, 2017 at 17:11 Comment(2)
"communicates with an API via AJAX... there is no server involved". There is a server, for the API. Is it not your own server / API?Medawar
Yep -- I'll clarify that in the question. I'm assuming there are servers for the CDN too? But I don't control them. I do have complete control over the API server.Skylab
M
17

I could create a GET endpoint to retrieve the token, but it seems like an attacker could simply access that endpoint and use the token it provides?

Correct. But CSRF tokens are not meant to be secret. They only exist to confirm an action is performed in the order expected by one user (e.g. a form POST only follows a GET request for the form). Even on a dynamic website an attacker could submit their own GET request to a page and parse out the CSRF token embedded in a form.

From OWASP:

CSRF is an attack that tricks the victim into submitting a malicious request. It inherits the identity and privileges of the victim to perform an undesired function on the victim's behalf.

It's perfectly valid to make an initial GET request on page load to get a fresh token and then submit it with the request performing an action.

If you want to confirm the identity of the person making the request you'll need authentication, which is a separate concern from CSRF.

Medawar answered 28/6, 2017 at 18:13 Comment(8)
Great points. It sounds like a CSRF token, even when embedded into an HTML page at page load, can't fully protect against cross-site request forgery, because an attacker could parse it out and use it in a POST request. A dedicated GET endpoint to provide the token is definitely easier for an attacker to grab/use, but it would provide more security than not enforcing CSRF tokens at all. Is that right? Thanks.Skylab
Yes, a dedicated GET request will at least confirm a user hasn't been tricked into sending the POST request.Medawar
Thanks. That is hugely helpful.Skylab
One quick follow up - I'm curious why this article says not to create a GET endpoint to fetch a token. Your logic makes sense to me; I can't see why they say not to do it here: github.com/pillarjs/…Skylab
They seem to be mixing authentication with CSRF. And they assume every user is authenticated. But either way, since your site is static you simply have to compromise to get the token after the page loads by using AJAX. If every AJAX request is also authenticated then no 3rd party can get the token. Since some of your POSTs are public you'll have to allow a public GET request for CSRF. An attacker could get a token and then POST to your API, but if the API is public there isn't much else you can do about it.Medawar
Sorry to revive this post: the CSRF token must be linked to the user? Otherwise any logged in user can generate a CSRF token and use it on someone else's accountHouseless
@Houseless Yes, the token should be associated with one and only one user. And each should be expired right after it's used.Medawar
I must say I do not agree with this suggestion. "They only exist to confirm an action is performed in the order expected by one user" - no, they come to defend against CSRF attacks, and if they don't you're doing it wrong. "Even on a dynamic website an attacker could submit their own GET request to a page and parse out the CSRF token embedded in a form" - that way the token will be linked to the attacker session and not the user session, and it will fail, if it doesn't fail on your site, you're doing something wrongSubscribe
C
0

My solution is as follows

Client [static html]

<script>
// Call script to GET Token and add to the form
fetch('https:/mysite/csrf.php')
.then(resp => resp.json())
.then(resp => {
    if (resp.token) {
        const csrf = document.createElement('input');
        csrf.name = "csrf";
        csrf.type = "hidden";
        csrf.value = resp.token;
        document.forms[0].appendChild(csrf);
    }
});
</script>

The above can be modified to target a pre-existing csrf field. I use this to add to may pages with forms. The script assumes the first form on the page is the target so this would also need to be changed if required.

On the server to generate the CSRF (Using PHP : assumes > 7)

[CSRFTOKEN is defined in a config file. Example]

define('CSRFTOKEN','__csrftoken');

Server:

$root_domain = $_SERVER['HTTP_HOST'] ?? false;
$referrer = $_SERVER['HTTP_REFERER'] ?? false;

// Check that script was called by page from same origin
// and generate token if valid. Save token in SESSION and
// return to client
$token = false;
if ($root_domain && 
    $referrer && 
    parse_url($referrer, PHP_URL_HOST) == $root_domain) {
  $token = bin2hex(random_bytes(16));
  $_SESSION[CSRFTOKEN] = $token;
}

header('Content-Type: application/json');
die(json_encode(['token' => $token]));

Finally in the code that processes the form

session_start();

// Included for clarity - this would typically be in a config
define('CSRFTOKEN', '__csrftoken');

$root_domain = $_SERVER['HTTP_HOST'] ?? false;
$referrer = parse_url($_SERVER['HTTP_REFERER'] ?? '', PHP_URL_HOST);

// Check submission was from same origin
if ($root_domain !== $referrer) {
    // Invalid attempt
    die();
}

// Extract and validate token
$token = $_POST[CSRFTOKEN] ?? false;
$sessionToken = $_SESSION[CSRFTOKEN] ?? false;
if (!empty($token) && $token === $sessionToken) {
  // Request is valid so process it
}

// Invalidate the token  
$_SESSION[CSRFTOKEN] = false;
unset($_SESSION[CSRFTOKEN]);
Consumerism answered 19/5, 2021 at 8:21 Comment(0)
Z
-1

There is very good explanation for same, Please check
https://cloudunder.io/blog/csrf-token/

from my understanding it seems static site won't face any issue with CSRF due to CORS restriction, if we have added X-Requested-With flag.
There is one more issue i would like to highlight here,

How to protect your api which is getting called from Mobile app as well as Static site?

As api is publicly exposed and you want to make sure only allowed user's should be calling it.
There is some check we can add at our API service layer for same

1) For AJAX request(From Static site) check for requesting domain, so only allowed sites can access it
2) For Mobile request use HMAC token, read more here
http://googleweblight.com/i?u=http://www.9bitstudios.com/2013/07/hmac-rest-api-security/&hl=en-IN

Zel answered 14/2, 2018 at 14:49 Comment(3)
-1 CORS restriction won't protect from CSRF. CORS prevents you from reading the result of a request, but not from making a request. That is impossible because CORS is sent in the response headers, hence the response must have been generated before the browser can read the CORS data. Thus a malicious site can make CSRF attacks to endpoints that change state despite CORS (eg bank.com/pay?amount=1000&to=Alice). They can't read the result, but they can observe the change in state in other ways- ie whether Alice has $1000 in her account.Mercantile
@Mercantile Using custom request header is one of the recommended way by OWASP. Even though the request is made, the server will check the custom request header. You can only add header with XHR while CORS prevents other side to make a request with custom request header.Designing
I'm not sure I understand; what would you put inside this custom request header? There's nothing to stop a malicious agent from adding the particular custom header to a client-side request from their domain, unless you use web tokens generated from your domain. Generate the web token from your domain, user can only read it if they pass CORS, they make a subsequent request with the token, you then know they're legit because they couldn't have gotten a valid request token without being on your domain due to CORS.Mercantile
A
-1

As suggested by OWASP you can use a Double Submit Cookie.

https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie

Afterpiece answered 3/7, 2023 at 19:48 Comment(1)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Hydrograph

© 2022 - 2024 — McMap. All rights reserved.