├── README.md └── csrf.conf /README.md: -------------------------------------------------------------------------------- 1 | csrf-nginx-redis-lua 2 | ==================== 3 | 4 | A simple nginx conf file to allow your backend (Varnish, Apache Traffic Server, etc) to not worry about CSRF tokens and put the onus on the front (nginx) instance -------------------------------------------------------------------------------- /csrf.conf: -------------------------------------------------------------------------------- 1 | 2 | server { 3 | listen 80; 4 | root /root/to/your/docroot; 5 | 6 | proxy_redirect off; 7 | proxy_intercept_errors on; 8 | proxy_set_header Host $host; 9 | proxy_set_header X-Real-IP $remote_addr; 10 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 11 | proxy_set_header X-Forwarded-Proto $scheme; 12 | proxy_set_header Request-URI $request_uri; 13 | proxy_set_header X-Backend example; 14 | proxy_pass_header Set-Cookie; 15 | 16 | location ~ \..*/.*\.php$ { 17 | return 403; 18 | } 19 | 20 | location / { 21 | # This is cool because no php is touched for static content 22 | expires max; 23 | try_files $uri @backend; 24 | } 25 | 26 | location /validate-csrf { 27 | # Only accessible as a subrequest 28 | internal; 29 | content_by_lua 30 | ' 31 | if ngx.var.cookie_csrf then 32 | -- user has a CSRF cookie, validate it 33 | local redis = require "redis" 34 | local client = redis.connect("127.0.0.1", 6379) 35 | local csrf_cookie = "csrf_" .. ngx.var.cookie_csrf 36 | local value = client:get(csrf_cookie) 37 | if value then 38 | ngx.say(value) -- useful for testing 39 | return ngx.exit(ngx.HTTP_OK) 40 | end 41 | end 42 | -- no cookie or csrf provided was not found 43 | return ngx.exit(ngx.HTTP_NOT_FOUND) 44 | '; 45 | } 46 | 47 | location @backend { 48 | # You can't set variables in nginx dynamically, so set this up as empty first 49 | set $csrf_validate ""; 50 | access_by_lua 51 | ' 52 | if ngx.req.get_method() == "POST" then 53 | -- set up forbidden as default 54 | ngx.var.csrf_validate = ngx.HTTP_FORBIDDEN 55 | if ngx.var.cookie_csrf then 56 | local res = ngx.location.capture("/validate-csrf") 57 | if ngx.HTTP_OK == res.status then 58 | ngx.req.read_body() 59 | local args = ngx.req.get_post_args() 60 | local posted_token = tostring(args["csrf"]) 61 | if posted_token == res.body then 62 | ngx.var.csrf_validate = ngx.HTTP_OK 63 | end 64 | end 65 | end 66 | end 67 | '; 68 | 69 | # Pass the result as a header to the backend 70 | proxy_set_header X-Csrf-Valid $csrf_validate; 71 | 72 | proxy_pass http://127.0.0.1:6081; 73 | 74 | # Now filter the response from the backend in as lightweight way as possible 75 | set $csrf_form_token ""; 76 | set $csrf_cookie_token ""; 77 | 78 | header_filter_by_lua 79 | ' 80 | if ngx.var.upstream_http_x_set_csrf then 81 | -- the backend requested a CSRF token be set 82 | local csrf_cookie_token = nil 83 | if ngx.var.cookie_csrf then 84 | -- they have a cookie, just re-use it 85 | local csrf_cookie_token = ngx.var.cookie_csrf 86 | end 87 | 88 | local resty_random = require "resty.random" 89 | local str = require "resty.string" 90 | 91 | if not csrf_cookie_token then 92 | -- no valid csrf cookie found, let us make one 93 | 94 | local cookie_random = resty_random.bytes(16,true) 95 | 96 | while cookie_random == nil do 97 | -- attempt to generate 16 bytes of 98 | -- cryptographically strong (enough) random data 99 | cookie_random = resty_random.bytes(16,true) 100 | end 101 | 102 | ngx.var.csrf_cookie_token = str.to_hex(cookie_random) 103 | end 104 | 105 | -- we are about to mess around with the content of the page 106 | -- so we need to clear this as it will be wrong 107 | ngx.header.Content_Length = "" 108 | -- set the Cookie for the CSRF token 109 | ngx.header.Set_Cookie = "csrf=" .. ngx.var.csrf_cookie_token 110 | 111 | -- now generate one for the form token 112 | while form_random == nil do 113 | form_random = resty_random.bytes(16,true) 114 | end 115 | 116 | ngx.var.csrf_form_token = str.to_hex(form_random) 117 | -- save these two random numbers as key-value pairs in Redis 118 | local redis = require "redis" 119 | local client = redis.connect("127.0.0.1", 6379) -- change as appropriate 120 | client:set("csrf_" .. ngx.var.csrf_cookie_token, ngx.var.csrf_form_token) 121 | end 122 | '; 123 | 124 | # Parse the body for csrf placeholder(s) and replace them with a hash 125 | body_filter_by_lua 126 | ' 127 | if ngx.var.csrf_form_token then 128 | ngx.arg[1] = ngx.re.gsub(ngx.arg[1], "::csrf::", ngx.var.csrf_form_token) 129 | end 130 | '; 131 | } 132 | } 133 | --------------------------------------------------------------------------------