CSRF Protection
These are my notes & planning from studying the owasp cheat sheet.
Warning: This is not comprehensive AND i am not an IT security expert. You should read the owasp pages on CSRF protection.
Important: XSS can get around any CSRF protection, so also protect against XSS
My Rewrite of instructions
User Login & other Forms:
- GET /user/login/
- generate CSRF token
- add CSRF to
<form>
in hidden input - store CSRF token details in
$_SESSION
with expiry timestamp and target validation uri and http host- set to
$_SESSION['csrf-'.uniqid()]
to prevent collisions
- set to
- POST /user/login/
- get csrf from
$_POST
- get csrf token details from
$_SESSION
- Ensure
$_SESSION['csrf']['token']
matches$_POST['token']
-
'csrf'
should include uniqid as descrbied in step 1.3
-
- Ensure
$_SESSION['csrf']['expiry']
is less thantime()
- Ensure
$_SESSION['csrf']['uri']
==$_SERVER['REQUEST_URI']
(just PATH portion) - Ensure domain of
$_SERVER[HTTP_REFERER] => http://localhost:3332/user/login/
matches$_SERVER['HTTP_HOST']
- this should work both for clicking logout link & for submitting forms - unset
$_SESSION['csrf']
- get csrf from
User Logout:
I could use session-based (rather than request-based) csrf token to validate a request to /user/logout/
but this seems unnecessary. The risk is that smoeone may click a logout link in their email (or something) and be taken to my site & be logged out. While that is an inconvenience & I want to prevent it, it is not a security risk. It does not grant anyone access to anything & does not change the state of the user - it just invalidates the authentication cookie the current user has
- GET /user/logout/
- ensure
$_SERVER['HTTP_REFERER']
matches$_SERVER['HTTP_HOST']
- ensure
Notes
These are cleaned up, slimmed down notes.
- Session based vs Request Based: Session based means generate token once, then use same token in all forms. Request based means generate token when
GET /form/
and validate that token forPOST /form/
but generate a new token every timeGET /any-form/
is requested - DO NOT
- put csrf token in the url (
?token=RANDOM
) bc that will store it in browser history - store token in server logs
- put csrf token in a cookie
- put csrf token in the url (
- DO
- put csrf token in hidden input form field
- or put csrf token in custom header through ajax
- verify origin. domain portion of
$_SERVER['HTTP_REFERER']
should match$_SERVER['HTTP_HOST']
- Stateless (store token in cookie, which is NOT preferred but may be necessary sometimes)
- store token in session
- ENCRYPT the token before setting to cookie, will decrypt server-side to validate against session token
- use
SameSite: Strict
attribute for cookie - DO NOT set the domain for the cookie as that will open the cookie to sub-domains
- SECURE cookie (for https only)
- add host to cookie name:
www.taeluf.com-csrf-token=RANDOM
- set path of cookie, such as
/user/login/
so the cookie only sends to the target page
- Additional Security, user-interaction based (i.e. add a step to the process):
- re-authenticate with password
- CAPTCHA
- one-time token
My Notes from OWASP
Lengthier, more comprehensive notes that i have not cleaned up
-
Idea: in the csrf_check, just short-circuit the request &
exit
giving a link to the current page, (or maybe the referring page?) so a new per-request token can be generated & sent to the browser -
add CSRF Token to all state changing requests & validate them on the backend
-
Stateful, use Synchronizer Token Pattern
- generate token server side, (per-request is more secure than per-session)
- I might need per-session token for validating logout page, since there's no form to be submitted
- store token in server-side session
- session based:
- store csrf token in
$_SESSION
- add csrf token to user form
- verify
$_POST['token']
matches$_SESSION['token']
- store csrf token in
- request based:
- store csrf token in
$_SESSION['some_key']
with additional validation information:- time token was generated
- page it was generated for
- add
<input name="some_key" value="CSRF_TOKEN />
to form - verify
$_POST['some_key']
matches$_SESSION['some_key']['token']
& verify expiry
- store csrf token in
- DO NOT PUT CSRF TOKEN IN:
- url (?token=whatever)
- server logs
- cookies
- put csrf token in hidden input fields or in headers
- use js ajax to add custom headers for more security, instead of the hidden input field method
- generate token server side, (per-request is more secure than per-session)
-
Stateless ("If maintaining the state for CSRF token at server side is problematic")
- validate a cookie value against a request value
- When a user vists, generat a csrf token (secure pseudorandom) & set to a cookie (separate from session id)
- now require every TRANSACTION (POST request) include this csrf token value
- prevent XSS vulnerabilities as XSS can mitigate all CSRF protections
- do not use GET requests for any state-changing operations
- Improve Security: (could potentially use HMAC instead of encryption to reduce computation)
- set
$_COOKIE['csrf'] = encrypt(csrf_token(), 'salt')
instead of$_COOKIE['csrf'] = csrf_token()
- Perhaps set
<input value="encrypt(csrf_token(), 'salt2')"
- salt could be: server-specific value + remote ip + user agent (perhaps hash it with a salt so I'm not storing sensitive info like ip address + user agent)
- Cookie Attributes for the CSRF Token
- set
SameSite: Strict
- do NOT set the cookie specifically for a domain, bc that would allow sub-domains
- set
- SECURE cookie (so it only goes over https)
- add host prefix to the cookie name (like 'www.taeluf.com-csrftoken=thetoken')
- set path to the explicit path we're validating
/user/login/
for example - Verify origin
- use Origin and/or Referrer headers
- "Determining the origin the request is going to (target origin)." idk??
- check REFERRER header for unauthenticated requests
- if you have a proxy or headers are otherwise not present, you may need to use:
- environment configuration value of the origin domain
- host header value
- X-Forwarded-Host
- allow when origin is the expected domain OR null (potentially exploitable, but owasp seems to mostly approve of this)
- "Origin header is included for all cross origin requests but for same origin requests, in most browsers it is only included in POST/DELETE/PUT"
- set
-
Other Methods
- custom request headers with ajax for REST ... use JS to send all forms through ajax?? Browsers only allow same-origin js to set custom request headers
- user-interaction based:
- re-authenticate with password
- CAPTCHA
- one-time token
-
Additional Notes
- Login csrf is important & can use pre-sessions + including token in login form, then DELETE the session after the user is authenticated to prevent session fixation attacks