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:

  1. GET /user/login/
    1. generate CSRF token
    2. add CSRF to <form> in hidden input
    3. store CSRF token details in $_SESSION with expiry timestamp and target validation uri and http host
      • set to $_SESSION['csrf-'.uniqid()] to prevent collisions
  2. POST /user/login/
    1. get csrf from $_POST
    2. get csrf token details from $_SESSION
    3. Ensure $_SESSION['csrf']['token'] matches $_POST['token']
      • 'csrf' should include uniqid as descrbied in step 1.3
    4. Ensure $_SESSION['csrf']['expiry'] is less than time()
    5. Ensure $_SESSION['csrf']['uri'] == $_SERVER['REQUEST_URI'] (just PATH portion)
    6. 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
    7. unset $_SESSION['csrf']

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

  1. GET /user/logout/
    1. ensure $_SERVER['HTTP_REFERER'] matches $_SERVER['HTTP_HOST']

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 for POST /form/ but generate a new token every time GET /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
  • 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']
    • 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
    • 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
  • 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
      • 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"
  • 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