Skip to content
Create a gist now

Instantly share code, notes, and snippets.

A simple nginx host file that, using the lua module, handles CSRF, rather than the backend having to (and thus generally breaking caching by having to use Set-Cookie). Here, the front end takes care of CSRF, and sends an X-CSRF-Valid header to the backend regarding the validity of the POST, so that it is advisory (the backend then choose whether…
server {
listen 80;
root /root/to/your/docroot;
proxy_redirect off;
proxy_intercept_errors on;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Request-URI $request_uri;
proxy_set_header X-Backend example;
proxy_pass_header Set-Cookie;
location ~ \..*/.*\.php$ {
return 403;
location / {
# This is cool because no php is touched for static content
expires max;
try_files $uri @backend;
location /validate-csrf {
# Only accessible as a subrequest
if ngx.var.cookie_csrf then
-- user has a CSRF cookie, validate it
local redis = require "redis"
local client = redis.connect("", 6379)
local csrf_cookie = "csrf_" .. ngx.var.cookie_csrf
local value = client:get(csrf_cookie)
if value then
ngx.say(value) -- useful for testing
return ngx.exit(ngx.HTTP_OK)
-- no cookie or csrf provided was not found
return ngx.exit(ngx.HTTP_NOT_FOUND)
location @backend {
# You can't set variables in nginx dynamically, so set this up as empty first
set $csrf_validate "";
if ngx.req.get_method() == "POST" then
-- set up forbidden as default
ngx.var.csrf_validate = ngx.HTTP_FORBIDDEN
if ngx.var.cookie_csrf then
local res = ngx.location.capture("/validate-csrf")
if ngx.HTTP_OK == res.status then
local args = ngx.req.get_post_args()
local posted_token = tostring(args["csrf"])
if posted_token == res.body then
ngx.var.csrf_validate = ngx.HTTP_OK
# Pass the result as a header to the backend
proxy_set_header X-Csrf-Valid $csrf_validate;
# Now filter the response from the backend in as lightweight way as possible
set $csrf_form_token "";
set $csrf_cookie_token "";
if ngx.var.upstream_http_x_csrf_tokenize then
-- the backend requested a CSRF token be set
local csrf_cookie_token = nil
if ngx.var.cookie_csrf then
-- they have a cookie, just re-use it
local csrf_cookie_token = ngx.var.cookie_csrf
local resty_random = require "resty.random"
local str = require "resty.string"
if not csrf_cookie_token then
-- no valid csrf cookie found, let us make one
local cookie_random = resty_random.bytes(16,true)
while cookie_random == nil do
-- attempt to generate 16 bytes of
-- cryptographically strong (enough) random data
cookie_random = resty_random.bytes(16,true)
ngx.var.csrf_cookie_token = str.to_hex(cookie_random)
-- we are about to mess around with the content of the page
-- so we need to clear this as it will be wrong
ngx.header.Content_Length = ""
-- set the Cookie for the CSRF token
ngx.header.Set_Cookie = "csrf=" .. ngx.var.csrf_cookie_token
ngx.header.tokenize = ngx.var.upstream_http_x_csrf_tokenize
-- now generate one for the form token
while form_random == nil do
form_random = resty_random.bytes(16,true)
ngx.var.csrf_form_token = str.to_hex(form_random)
local redis = require "redis"
local client = redis.connect("", 6379)
client:set("csrf_" .. ngx.var.csrf_cookie_token, ngx.var.csrf_form_token)
# Parse the body for csrf placeholder(s) and replace them with a hash
local csrf_form_token = ngx.var.csrf_form_token or ""
if ngx.var.csrf_form_token then
local placeholder = ngx.var.upstream_http_x_csrf_tokenize or ""
ngx.arg[1] =[1], placeholder, ngx.var.csrf_form_token)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.