Last active
August 24, 2021 14:24
-
-
Save dustyfresh/2b6534e97775519c40d4375b8e6c67b6 to your computer and use it in GitHub Desktop.
Simple & experimental Web Application Firewall using Cloudflare's edge workers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* | |
* Web Application Firewall built with Cloudflare workers | |
* | |
* Author: < https://twitter.com/dustyfresh > | |
* | |
* License: GPLv3 < https://www.gnu.org/licenses/gpl-3.0.en.html > | |
* | |
* Cloudflare worker documentation: | |
* < https://developers.cloudflare.com/workers/about/ > | |
* | |
* Event logging is with Loggly | |
* < https://www.loggly.com/docs/http-endpoint/ > | |
* | |
*/ | |
/* | |
Start of variable config | |
- Each request starts with a risk score of 0 | |
- Any request with a risk score greater than safe_score will be dropped | |
*/ | |
var score = 0; | |
var safe_score = 50; | |
// Set this to 1 if you are using static hosting like S3 that can't process POST requests. | |
// Set to 0 if your backend will handle POST requests | |
var no_post = 0; | |
// loggly HTTP/S Event Endpoint to send logs to | |
// https://www.loggly.com/docs/http-endpoint/ | |
var LOGGLY_ENDPOINT = 'changeme' | |
// error handling | |
function handle_error(err){ | |
console.log(err); | |
} | |
// event logging | |
function log_violation(msg){ | |
console.log(msg); | |
} | |
function high_risk_event(input){ | |
// things that go here should always have higher weight because it's definitely | |
// considered bad. | |
var bad_input = [ | |
'%00', | |
'eval(', | |
'alert(', | |
'<?', | |
'javascript:', | |
'<script>', | |
'\00', | |
'system(', | |
'file://', | |
'php://', | |
'gopher://', | |
'ftp://', | |
'sftp://', | |
'zlib://', | |
'data://', | |
'glob://', | |
'$(', | |
'`', | |
'cmd.exe', | |
] | |
bad_input.forEach(function(sig){ | |
if(input.includes(sig)){ | |
score += 100; | |
log_violation('detected '+sig+' in the user-agent header'); | |
} | |
}); | |
} | |
// Process user-agent for malicious things | |
function process_user_agent(ua){ | |
high_risk_event(ua); | |
// process user-agent with our list of regular expression signatures | |
var bad_agent_regexp = [ | |
'python', | |
'curl', | |
'java', | |
'wget', | |
'lynx', | |
'eval', | |
'fake', | |
'w00t', | |
'perl', | |
'spider', // arachnophobia was the best movie of all time | |
'burp', | |
'acunetix', | |
'desu', | |
'wpscan', | |
'dirbuster', | |
'sqlmap', | |
'evil', | |
'masscan', | |
'requests', | |
'shodan', | |
'scan.lol', | |
'nikto', | |
'nmap', | |
'`', | |
"'{1}", // start of some sqli sigs | |
'union', | |
'update', | |
'delete', | |
'insert', | |
'table', | |
'from', | |
'ascii', | |
'hex', | |
'drop', | |
'eval', | |
] | |
bad_agent_regexp.forEach(function(sig){ | |
var regexp = new RegExp(sig); | |
if(regexp.test(ua)){ | |
score += 100; | |
log_violation('detected '+sig+' in the user-agent header'); | |
} | |
}); | |
} | |
// Process URL | |
function process_url(url){ | |
high_risk_event(url); | |
var bad_url_sigs = [ | |
'..\/{1,}etc', | |
"'{1}" | |
] | |
bad_url_sigs.forEach(function(sig){ | |
var regexp = new RegExp(sig); | |
if(regexp.test(url)){ | |
score += 100; | |
log_violation('detected '+sig+' in the url'); | |
} | |
}); | |
} | |
// Process POST input before sending to the backend | |
function process_post(postData){ | |
high_risk_event(postData); | |
// start of regexp sigs | |
var bad_post_sigs = [ | |
'..\/{1,}etc', | |
"'{1}" | |
] | |
bad_post_sigs.forEach(function(sig) { | |
var regexp = new RegExp(sig); | |
if(regexp.test(postData)){ | |
score += 100; | |
log_violation('detected '+sig+' in POST data'); | |
} | |
}); | |
} | |
// start the CF worker event listener | |
addEventListener('fetch', event => { | |
event.respondWith(fetchAndApply(event.request)) | |
}); | |
async function fetchAndApply(request) { | |
// We catch the exception and set ua to 0 if there | |
// is not user-agent header in the request | |
try { | |
// start user-agent analysis | |
var ua = request.headers.get('user-agent').toLowerCase(); | |
process_user_agent(ua, score); | |
} catch(err) { | |
var ua = 0; | |
} | |
// start URL analysis | |
var url = request.url.toLowerCase(); | |
process_url(decodeURIComponent(url), score); | |
// inspect POST requests for bad things | |
if(request.method == 'POST'){ | |
if(no_post == 1){ | |
return new Response('Method not allowed', {status: 405, statusText: 'denied'}); | |
} else { | |
let body = await request.text() | |
let formData = new URLSearchParams(body) | |
process_post(decodeURIComponent(formData)); | |
// we log all POST data to loggly (todo: change this to be json data that is sent to loggly) | |
let headers = {'Content-Type': 'content-type:text/plain' } | |
const init = { method: 'POST', headers: headers, body: '{ "event": "post_request", "score": ' + score + ', "payload": "' + decodeURIComponent(body) + '", "url": "' + decodeURIComponent(request.url) + '" }' } | |
const response = await fetch(LOGGLY_ENDPOINT, init); | |
// check request threat score | |
if(score > safe_score){ | |
// return 403 page if POST check does not pass the process_post function | |
let headers = {'Content-Type': 'content-type:text/plain' } | |
const init = { method: 'POST', headers: headers, body: '{ "event": "firewall", "score": ' + score + ', "payload": "' + decodeURIComponent(body) + '", "url": "' + decodeURIComponent(request.url) + '" }' } | |
const response = await fetch(LOGGLY_ENDPOINT, init); | |
return new Response('(╯°□°)╯︵ ┻━┻', {status: 403, statusText: 'Forbidden'}); | |
} else { | |
// return request to backend with POST params since they are not bad | |
let newRequest = new Request(request, { body }) | |
return fetch(newRequest); | |
} | |
} | |
} else { | |
// proceed with GET request scoring | |
if(score > safe_score){ | |
let headers = {'Content-Type': 'content-type:text/plain' } | |
const init = { method: 'POST', headers: headers, body: '{ "event": "firewall", "score": ' + score + ', "url": "' + decodeURIComponent(request.url) + '" }' } | |
const response = await fetch(LOGGLY_ENDPOINT, init); | |
return new Response('(╯°□°)╯︵ ┻━┻', {status: 403, statusText: 'Forbidden'}); | |
} else { | |
return fetch(request); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Nice gist! So the
workerWAF
scans for bad signatures in the POST and GET requests. It's acting as a MITM listener. I assume all the requests will be through SSL, how would you inspect a request if it's encrypted? Or when a request gets toworkWAF
, it's already unencrypted? Pardon my lack of knowledge of how CF works...