public
Created

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 or not to blow up about it). To cut down processing on every request, the CSRF parsing requires the backend to send an X-CSRF-Tokenize header, which also describes the token to pass for. Thus, a page that sends a header of X-CSRF-Tokenize : '::csrf::' will have all instances of '<input type="hidden" name="csrf" value="::csrf::" /> populated with a random hash.

  • Download Gist
csrf-lua.conf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
 
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
internal;
content_by_lua
'
if ngx.var.cookie_csrf then
-- user has a CSRF cookie, validate it
local redis = require "redis"
local client = redis.connect("127.0.0.1", 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)
end
end
-- 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 "";
access_by_lua
'
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
ngx.req.read_body()
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
end
end
end
end
';
 
# Pass the result as a header to the backend
proxy_set_header X-Csrf-Valid $csrf_validate;
 
proxy_pass http://127.0.0.1:6081;
 
# Now filter the response from the backend in as lightweight way as possible
set $csrf_form_token "";
set $csrf_cookie_token "";
 
header_filter_by_lua
'
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
end
 
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)
end
 
ngx.var.csrf_cookie_token = str.to_hex(cookie_random)
end
 
-- 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)
end
 
ngx.var.csrf_form_token = str.to_hex(form_random)
 
local redis = require "redis"
local client = redis.connect("127.0.0.1", 6379)
client:set("csrf_" .. ngx.var.csrf_cookie_token, ngx.var.csrf_form_token)
end
';
 
# Parse the body for csrf placeholder(s) and replace them with a hash
body_filter_by_lua
'
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] = ngx.re.gsub(ngx.arg[1], placeholder, ngx.var.csrf_form_token)
end
';
}

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.