Skip to content

Instantly share code, notes, and snippets.

@dustyfresh
Last active August 24, 2021 14:24
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save dustyfresh/2b6534e97775519c40d4375b8e6c67b6 to your computer and use it in GitHub Desktop.
Save dustyfresh/2b6534e97775519c40d4375b8e6c67b6 to your computer and use it in GitHub Desktop.
Simple & experimental Web Application Firewall using Cloudflare's edge workers
/*
*
* 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);
}
}
}
@khaaliisa
Copy link

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 to workWAF, it's already unencrypted? Pardon my lack of knowledge of how CF works...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment