Skip to content

Instantly share code, notes, and snippets.

@lcrilly
Last active April 14, 2021 17:54
Show Gist options
  • Save lcrilly/7c721224902999bc9abf69a6686878b7 to your computer and use it in GitHub Desktop.
Save lcrilly/7c721224902999bc9abf69a6686878b7 to your computer and use it in GitHub Desktop.
Adding cookie security with NGINX and NGINX Plus

Cookie Security with NGINX and NGINX Plus

This is a complete demo of 2 different cookie security techniques:

  1. Cookie jar - NGINX Plus stores new cookies in the key-value store and issues the client an opaque reference to access them
  2. Signed cookies - NGINX creates signatures for all new cookies and validates that presented cookies match the signature

Requires NGINX Plus with JavaScript module (njs 0.5.1+)

Demo

NGINX is configured as a reverse proxy to an "Random Emoji App". On first access, the app will produce an emoji, and send cookies for session state. Repeat visits will show the same cookie.

  • By default, all requests will send cookies directly to the client, in the normal manner.
  • Requests to /jar will keep session state in the key-value store, the client receives a single reference cookie
  • Requests to /sign will send an additional cookie (sig. prefix) with a cryptographic signature that must be present with future client requests
export default { cookieJar, signCookies, validateCookies }
function cookieJar(r) {
// Replace Set-Cookie response headers with an opaque reference
if (r.headersOut['Set-Cookie'].length) {
var kvs = [];
r.headersOut['Set-Cookie'].forEach(c => kvs.push(c.split(';')[0])); // Omit cookie flags
r.variables.new_session = kvs.join('; '); // Store in keyval cookie jar
r.headersOut['Set-Cookie'] = "session=" + r.variables.request_id + "; " + r.variables.session_cookie_flags;
}
}
function signCookies(r) {
var c = require('crypto');
var kv, sc = [];
if (r.headersOut['Set-Cookie'].length) {
r.headersOut['Set-Cookie'].forEach(function(cookie){
kv = cookie.split('=');
sc.push("sig." + kv[0] + '=' + c.createHmac('sha256', r.variables.signing_key).update(kv[1].split(';')[0]).digest('base64url'));
sc.push(cookie); // This is inefficient but cannot find a way to append to r.headersOut['Set-Cookie'] object
});
r.headersOut['Set-Cookie'] = sc;
}
}
function validateCookies(r) {
if (typeof(r.headersIn['Cookie']) == "undefined") {
r.log("NO COOKIES TO VALIDATE");
return '';
} else {
var fail = false, kv = [], raw = {}, signed = {};
// Convert Cookie header into matching objects of raw and signed cookies
var cookies = r.headersIn['Cookie'].split('; ');
cookies.forEach(function(cookie){
r.log("FOUND " + cookie);
kv = cookie.split('=');
if (kv[0].startsWith('sig.')) {
r.log("ADDING " + kv[0] + " TO SIGNED OBJECT");
signed[kv[0].slice(4)] = kv[1];
} else {
r.log("ADDING " + kv[0] + " TO RAW OBJECT");
raw[kv[0]] = kv[1];
}
});
// Loop through each raw cookie and ensure there is a corresponding signature
var c = require('crypto');
Object.keys(raw).forEach(function(cookie){
r.log("CHECKING " + cookie + ": " + raw[cookie] + " == " + signed[cookie]);
if (typeof(signed[cookie]) == "undefined") {
r.warn("COOKIE " + cookie + " has no signature");
fail = true;
} else {
if (signed[cookie] != c.createHmac('sha256', r.variables.signing_key).update(raw[cookie]).digest('base64url')) {
r.warn("COOKIE " + cookie + " has invalid signature");
fail = true;
}
}
});
if (fail) {
return '';
} else {
// Return raw cookies ready to be proxied
return Object.keys(raw).map(key => `${key}=${raw[key]}`).join('; ');
}
}
}
upstream emoji_app {
zone backend_app 64k;
server 127.0.0.1:9000;
}
js_import conf.d/csecure.js;
js_set $raw_cookies secure.validateCookies;
map 1 $signing_key {
default "somerandomsigningkey";
}
keyval_zone zone=cookie_jar:128K timeout=90s;
keyval $cookie_session $cookies zone=cookie_jar;
keyval $request_id $new_session zone=cookie_jar;
server {
listen 80;
# Use keyval as a cookie jar for cookies emitted from backend app
location /jar {
proxy_pass http://emoji_app;
proxy_set_header Cookie $cookies;
set $session_cookie_flags "HttpOnly; SameSite=Lax;";
js_header_filter secure.cookieJar;
}
# Sign and validate cookies emitted from backend app
location /sign {
proxy_set_header Cookie $raw_cookies; # Signature failure deletes cookies
proxy_pass http://emoji_app;
js_header_filter secure.signCookies;
}
# Normal browser cookie handling/interaction
location / {
proxy_pass http://emoji_app;
}
error_log /var/log/nginx/error.log warn;
}
server {
listen 8080;
default_type application/json;
location /api/ {
api write=on;
}
location / {
return 200 '["/api/"]\n';
}
}
# Emoji App
split_clients $request_id $new_emoji {
5% 26BE; # ⚾
5% 26FA; # β›Ί
5% 1F30E; # 🌎
5% 1F33D; # 🌽
5% 1F3C4; # πŸ„
5% 1F400; # πŸ€
5% 1F527; # πŸ”§
5% 1F5FF; # πŸ—Ώ
5% 1F6E9; # πŸ›©
5% 1F933; # 🀳
10% 1F680; # πŸš€
10% 1F369; # 🍩
10% 1F69C; # 🚜
10% 1F995; # πŸ¦•
* 1F4A9; # πŸ’©
}
map $cookie_emoji $emoji_hex {
"" $new_emoji;
default $cookie_emoji;
}
log_format cookies '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" "$http_cookie"';
server {
listen 9000;
access_log /var/log/nginx/access.log cookies;
location / {
default_type text/html; # Cannot use add_header Content-Type when there is another add_header inside if{}
if ($cookie_emoji = "") { # Adding more than one of these does
add_header Set-Cookie "emoji=$emoji_hex; Path=/; HttpOnly; SameSite=Lax";
add_header Set-Cookie "session_started=$time_iso8601; Path=/; HttpOnly; SameSite=Lax";
}
return 200 '<!DOCTYPE html>\n<html>\n<head>\n<title>Emoji Surprise!</title><meta http-equiv="refresh" content="2;url="$uri"></head><body bgcolor="#$request_id;">
<p style="font-family:sans-serif;font-size:40px">Your emoji surprise!</p>
<p style="font-size:175px">&nbsp;&#x$emoji_hex;</p>
<tt>Cookie: $http_cookie</tt>\n</body>\n</html>\n';
}
location = /favicon.ico {
add_header Content-Type image/svg+xml;
return 200 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="90">🎲</text></svg>';
}
}
# vim: syntax=nginx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment