From dcaa6e9ddcf3e7e1227a4e9e21fc6369543285a2 Mon Sep 17 00:00:00 2001 From: Mic Date: Mon, 24 Aug 2020 17:19:25 -0400 Subject: [PATCH 1/9] Cleaned up CSP server apps, now a monolith --- {client.csp.demo => csp.demo}/app.js | 4 ++-- .../middleware/exerciseSetCsp.js | 0 .../middleware/generateNonce.js | 0 .../middleware/responseHeaders.js | 0 {client.csp.demo => csp.demo}/routes/exercise.js | 0 {client.csp.demo => csp.demo}/static/alertsolve.js | 0 {client.csp.demo => csp.demo}/static/bulma.min.css | 0 {client.csp.demo => csp.demo}/static/cspForm.js | 0 {client.csp.demo => csp.demo}/static/main.js | 0 {client.csp.demo => csp.demo}/views/_base.njk | 0 {client.csp.demo => csp.demo}/views/ex1.njk | 0 {client.csp.demo => csp.demo}/views/ex2.njk | 0 {client.csp.demo => csp.demo}/views/index.njk | 0 {client.csp.demo => csp.demo}/views/set-csp.njk | 0 index.js | 14 ++++---------- sample.env | 3 +-- server.csp.demo/app.js | 6 ------ 17 files changed, 7 insertions(+), 20 deletions(-) rename {client.csp.demo => csp.demo}/app.js (96%) rename {client.csp.demo => csp.demo}/middleware/exerciseSetCsp.js (100%) rename {client.csp.demo => csp.demo}/middleware/generateNonce.js (100%) rename {client.csp.demo => csp.demo}/middleware/responseHeaders.js (100%) rename {client.csp.demo => csp.demo}/routes/exercise.js (100%) rename {client.csp.demo => csp.demo}/static/alertsolve.js (100%) rename {client.csp.demo => csp.demo}/static/bulma.min.css (100%) rename {client.csp.demo => csp.demo}/static/cspForm.js (100%) rename {client.csp.demo => csp.demo}/static/main.js (100%) rename {client.csp.demo => csp.demo}/views/_base.njk (100%) rename {client.csp.demo => csp.demo}/views/ex1.njk (100%) rename {client.csp.demo => csp.demo}/views/ex2.njk (100%) rename {client.csp.demo => csp.demo}/views/index.njk (100%) rename {client.csp.demo => csp.demo}/views/set-csp.njk (100%) delete mode 100644 server.csp.demo/app.js diff --git a/client.csp.demo/app.js b/csp.demo/app.js similarity index 96% rename from client.csp.demo/app.js rename to csp.demo/app.js index 8b7e69a..cf412b6 100644 --- a/client.csp.demo/app.js +++ b/csp.demo/app.js @@ -6,7 +6,7 @@ const resHeaders = require('./middleware/responseHeaders') const generateNonce = require('./middleware/generateNonce') const app = express() -const jucksEnv = new nunjucks.Environment(new nunjucks.FileSystemLoader('client.csp.demo/views')) +const jucksEnv = new nunjucks.Environment(new nunjucks.FileSystemLoader('csp.demo/views')) const supportedDirectives = ['default-src', 'script-src', 'style-src', 'img-src', 'connect-src', 'font-src', 'object-src', 'media-src', 'child-src', 'frame-ancestors', 'form-action', 'base-uri'] @@ -18,7 +18,7 @@ app.use(resHeaders({ 'Cache-Control': 'no-cache', })) -app.use(express.static('client.csp.demo/static')) +app.use(express.static('csp.demo/static')) global.csp = `default-src 'self'` global.cspDirectives = { 'default-src': `'self'`, 'use-default-src': 'on' } diff --git a/client.csp.demo/middleware/exerciseSetCsp.js b/csp.demo/middleware/exerciseSetCsp.js similarity index 100% rename from client.csp.demo/middleware/exerciseSetCsp.js rename to csp.demo/middleware/exerciseSetCsp.js diff --git a/client.csp.demo/middleware/generateNonce.js b/csp.demo/middleware/generateNonce.js similarity index 100% rename from client.csp.demo/middleware/generateNonce.js rename to csp.demo/middleware/generateNonce.js diff --git a/client.csp.demo/middleware/responseHeaders.js b/csp.demo/middleware/responseHeaders.js similarity index 100% rename from client.csp.demo/middleware/responseHeaders.js rename to csp.demo/middleware/responseHeaders.js diff --git a/client.csp.demo/routes/exercise.js b/csp.demo/routes/exercise.js similarity index 100% rename from client.csp.demo/routes/exercise.js rename to csp.demo/routes/exercise.js diff --git a/client.csp.demo/static/alertsolve.js b/csp.demo/static/alertsolve.js similarity index 100% rename from client.csp.demo/static/alertsolve.js rename to csp.demo/static/alertsolve.js diff --git a/client.csp.demo/static/bulma.min.css b/csp.demo/static/bulma.min.css similarity index 100% rename from client.csp.demo/static/bulma.min.css rename to csp.demo/static/bulma.min.css diff --git a/client.csp.demo/static/cspForm.js b/csp.demo/static/cspForm.js similarity index 100% rename from client.csp.demo/static/cspForm.js rename to csp.demo/static/cspForm.js diff --git a/client.csp.demo/static/main.js b/csp.demo/static/main.js similarity index 100% rename from client.csp.demo/static/main.js rename to csp.demo/static/main.js diff --git a/client.csp.demo/views/_base.njk b/csp.demo/views/_base.njk similarity index 100% rename from client.csp.demo/views/_base.njk rename to csp.demo/views/_base.njk diff --git a/client.csp.demo/views/ex1.njk b/csp.demo/views/ex1.njk similarity index 100% rename from client.csp.demo/views/ex1.njk rename to csp.demo/views/ex1.njk diff --git a/client.csp.demo/views/ex2.njk b/csp.demo/views/ex2.njk similarity index 100% rename from client.csp.demo/views/ex2.njk rename to csp.demo/views/ex2.njk diff --git a/client.csp.demo/views/index.njk b/csp.demo/views/index.njk similarity index 100% rename from client.csp.demo/views/index.njk rename to csp.demo/views/index.njk diff --git a/client.csp.demo/views/set-csp.njk b/csp.demo/views/set-csp.njk similarity index 100% rename from client.csp.demo/views/set-csp.njk rename to csp.demo/views/set-csp.njk diff --git a/index.js b/index.js index 64f5213..0533944 100644 --- a/index.js +++ b/index.js @@ -4,15 +4,13 @@ const corsApiPort = process.env.CORS_API_PORT || 3020 const corsClientPort = process.env.CORS_CLIENT_PORT || 3021 const oauthProviderPort = process.env.OAUTH_PROVIDER_PORT || 3030 const oauthClientPort = process.env.OAUTH_CLIENT_PORT || 3031 -const cspServerPort = process.env.CSP_SERVER_PORT || 3040 -const cspClientPort = process.env.CSP_CLIENT_PORT || 3041 +const cspAppPort = process.env.CSP_APP_PORT || 3041 const corsApi = require('./api.cors.demo/app') const corsClient = require('./client.cors.demo/app') // const oauthProvider = require('./auth-server.oauth.demo/app') // const oauthClient = require('./client.oauth.demo/app') -const cspServer = require('./server.csp.demo/app') -const cspClient = require('./client.csp.demo/app') +const cspClient = require('./csp.demo/app') corsApi.listen(corsApiPort, () => console.log(`CORS demo API server listening on port ${corsApiPort}`) @@ -30,10 +28,6 @@ oauthClient.listen(oauthClientPort, () => console.log(`OAuth demo client available on port ${oauthClientPort}`) )*/ -cspServer.listen(cspServerPort, () => - console.log(`CSP demo server available on port ${cspServerPort}`) -) - -cspClient.listen(cspClientPort, () => - console.log(`CSP demo client available on port ${cspClientPort}`) +cspClient.listen(cspAppPort, () => + console.log(`CSP demo app available on port ${cspAppPort}`) ) diff --git a/sample.env b/sample.env index 0cd716f..9942360 100644 --- a/sample.env +++ b/sample.env @@ -2,5 +2,4 @@ CORS_API_PORT=3020 CORS_CLIENT_PORT=3021 OAUTH_PROVIDER_PORT=3030 OAUTH_CLIENT_PORT=3031 -CSP_SERVER_PORT=3040 -CSP_CLIENT_PORT=3041 \ No newline at end of file +CSP_APP_PORT=3041 diff --git a/server.csp.demo/app.js b/server.csp.demo/app.js deleted file mode 100644 index c124073..0000000 --- a/server.csp.demo/app.js +++ /dev/null @@ -1,6 +0,0 @@ -const express = require('express') -const app = express() - -app.get('/', (req, res) => res.send('Hello CSP Server!')) - -module.exports = app From 798aacc03b3400f43e7c4d64ef5f458ee3b6576c Mon Sep 17 00:00:00 2001 From: Mic Date: Mon, 24 Aug 2020 17:35:18 -0400 Subject: [PATCH 2/9] Converted CORS client index.html into Nunjucks view --- client.cors.demo/app.js | 16 +++++++++++++--- .../{static/index.html => views/index.njk} | 0 2 files changed, 13 insertions(+), 3 deletions(-) rename client.cors.demo/{static/index.html => views/index.njk} (100%) diff --git a/client.cors.demo/app.js b/client.cors.demo/app.js index 13956e9..f8deeb8 100644 --- a/client.cors.demo/app.js +++ b/client.cors.demo/app.js @@ -1,6 +1,16 @@ const express = require('express') -const client = express() +const nunjucks = require('nunjucks') -client.use(express.static('client.cors.demo/static')) +const app = express() +const jucksEnv = new nunjucks.Environment(new nunjucks.FileSystemLoader('client.cors.demo/views')) -module.exports = client +jucksEnv.express(app) +app.set('view engine', 'njk') + +app.use(express.static('client.cors.demo/static')) + +app.get('/', (req, res) => { + res.render('index') +}) + +module.exports = app diff --git a/client.cors.demo/static/index.html b/client.cors.demo/views/index.njk similarity index 100% rename from client.cors.demo/static/index.html rename to client.cors.demo/views/index.njk From 06f3b7b8c345f6ed87065b5430b52e515de19eee Mon Sep 17 00:00:00 2001 From: Mic Date: Tue, 25 Aug 2020 13:23:07 -0400 Subject: [PATCH 3/9] Added USE_TLS and CORS_API_HOST config options, auto-populating in CORS demo client --- client.cors.demo/app.js | 16 +++++++++++++++- client.cors.demo/views/index.njk | 16 ++++++++-------- sample.env | 2 ++ 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/client.cors.demo/app.js b/client.cors.demo/app.js index f8deeb8..fa8677d 100644 --- a/client.cors.demo/app.js +++ b/client.cors.demo/app.js @@ -4,13 +4,27 @@ const nunjucks = require('nunjucks') const app = express() const jucksEnv = new nunjucks.Environment(new nunjucks.FileSystemLoader('client.cors.demo/views')) +const apiHost = process.env.CORS_API_HOST || 'api.cors.dem'; + jucksEnv.express(app) app.set('view engine', 'njk') app.use(express.static('client.cors.demo/static')) app.get('/', (req, res) => { - res.render('index') + res.render('index', { apiHost: apiHost, protocol: stringToBool(process.env.USE_TLS) ? 'https' : 'http' }) }) module.exports = app + +function stringToBool(str) { + let test = str.toUpperCase().trim(); + if(["TRUE","Y","YES","1"].indexOf(test) > -1) { + return true + } else if (["FALSE","N","NO","0"].indexOf(test) > -1) { + return false + } else { + console.error(`Invalid value in stringToBool: ${str}`) + return undefined + } + } \ No newline at end of file diff --git a/client.cors.demo/views/index.njk b/client.cors.demo/views/index.njk index 14c648e..d61098c 100644 --- a/client.cors.demo/views/index.njk +++ b/client.cors.demo/views/index.njk @@ -27,7 +27,7 @@
- +
@@ -204,7 +204,7 @@ })(); let _apiUrl = (function() { let textbox = document.getElementById('apiUrlTextbox'); - let currentApiUrl = 'api.cors.dem'; + let currentApiUrl = '{{ apiHost }}'; textbox.value = currentApiUrl; return { set: function(val) { @@ -238,7 +238,7 @@ let req = new XMLHttpRequest(); req.addEventListener("load", updateResponseBox); - req.open('POST', `http:\\\\${_apiUrl.get()}\\auth\\login`); + req.open('POST', `{{ protocol }}\\\\${_apiUrl.get()}\\auth\\login`); req.withCredentials = true; req.send(JSON.stringify(body)); }, @@ -246,7 +246,7 @@ let req = new XMLHttpRequest(); req.addEventListener("load", updateResponseBox); req.addEventListener("error", showRequestError); - req.open('GET', `http:\\\\${_apiUrl.get()}\\${_corsPolicy.get()}\\object?ts=${Date.now()}`); + req.open('GET', `{{ protocol }}\\\\${_apiUrl.get()}\\${_corsPolicy.get()}\\object?ts=${Date.now()}`); req.withCredentials = true; req.send(); }, @@ -254,7 +254,7 @@ let payload = JSON.stringify(JSON.parse(document.getElementById('insertObjectJson').value)); let req = new XMLHttpRequest(); req.addEventListener("load", updateResponseBox); - req.open('POST', `http:\\\\${_apiUrl.get()}\\${_corsPolicy.get()}\\object`); + req.open('POST', `{{ protocol }}\\\\${_apiUrl.get()}\\${_corsPolicy.get()}\\object`); req.withCredentials = true; req.send(payload); }, @@ -262,7 +262,7 @@ let req = new XMLHttpRequest(); let uid = document.getElementById('getObjectUid').value; req.addEventListener("load", updateResponseBox); - req.open('GET', `http:\\\\${_apiUrl.get()}\\${_corsPolicy.get()}\\object\\${uid}?ts=${Date.now()}`); + req.open('GET', `{{ protocol }}\\\\${_apiUrl.get()}\\${_corsPolicy.get()}\\object\\${uid}?ts=${Date.now()}`); req.withCredentials = true; req.send(); }, @@ -271,7 +271,7 @@ let payload = JSON.stringify(JSON.parse(document.getElementById('updateObjectJson').value)); let uid = document.getElementById('updateObjectUid').value; req.addEventListener("load", updateResponseBox); - req.open('PUT', `http:\\\\${_apiUrl.get()}\\${_corsPolicy.get()}\\object\\${uid}`); + req.open('PUT', `{{ protocol }}\\\\${_apiUrl.get()}\\${_corsPolicy.get()}\\object\\${uid}`); req.withCredentials = true; req.send(payload); }, @@ -279,7 +279,7 @@ let req = new XMLHttpRequest(); let uid = document.getElementById('deleteObjectUid').value; req.addEventListener("load", updateResponseBox); - req.open('DELETE', `http:\\\\${_apiUrl.get()}\\${_corsPolicy.get()}\\object\\${uid}`); + req.open('DELETE', `{{ protocol }}\\\\${_apiUrl.get()}\\${_corsPolicy.get()}\\object\\${uid}`); req.withCredentials = true; req.send(); } diff --git a/sample.env b/sample.env index 9942360..9db1ed9 100644 --- a/sample.env +++ b/sample.env @@ -1,5 +1,7 @@ CORS_API_PORT=3020 +CORS_API_HOST=localhost:3021 CORS_CLIENT_PORT=3021 OAUTH_PROVIDER_PORT=3030 OAUTH_CLIENT_PORT=3031 CSP_APP_PORT=3041 +USE_TLS=TRUE \ No newline at end of file From 7cc4b3bc9dffeaffcf4828209e3084069d2512df Mon Sep 17 00:00:00 2001 From: Mic Date: Tue, 25 Aug 2020 14:13:52 -0400 Subject: [PATCH 4/9] Auto-generate CORS reflect pattern based on .env --- api.cors.demo/routes/pattern.js | 7 ++++++- client.cors.demo/views/index.njk | 12 ++++++------ sample.env | 1 + 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/api.cors.demo/routes/pattern.js b/api.cors.demo/routes/pattern.js index f562f6b..3212650 100644 --- a/api.cors.demo/routes/pattern.js +++ b/api.cors.demo/routes/pattern.js @@ -8,11 +8,16 @@ const sessionBouncer = require('../middleware/cookieSessionBouncer') const dataCtrlr = require('../controllers/data') +function escapeRegex(string) { + return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); +} + const corsOptions = { - origin: /cors\.dem$/, + origin: new RegExp(escapeRegex(process.env.CORS_CLIENT_HOST) + '$'), methods: 'GET,POST,PUT,DELETE', credentials: true } +console.log('corsOptions:', corsOptions); router.use(cors(corsOptions)) authSubRouter.use(sessionLoader) diff --git a/client.cors.demo/views/index.njk b/client.cors.demo/views/index.njk index d61098c..8910c93 100644 --- a/client.cors.demo/views/index.njk +++ b/client.cors.demo/views/index.njk @@ -238,7 +238,7 @@ let req = new XMLHttpRequest(); req.addEventListener("load", updateResponseBox); - req.open('POST', `{{ protocol }}\\\\${_apiUrl.get()}\\auth\\login`); + req.open('POST', `{{ protocol }}:\\\\${_apiUrl.get()}\\auth\\login`); req.withCredentials = true; req.send(JSON.stringify(body)); }, @@ -246,7 +246,7 @@ let req = new XMLHttpRequest(); req.addEventListener("load", updateResponseBox); req.addEventListener("error", showRequestError); - req.open('GET', `{{ protocol }}\\\\${_apiUrl.get()}\\${_corsPolicy.get()}\\object?ts=${Date.now()}`); + req.open('GET', `{{ protocol }}:\\\\${_apiUrl.get()}\\${_corsPolicy.get()}\\object?ts=${Date.now()}`); req.withCredentials = true; req.send(); }, @@ -254,7 +254,7 @@ let payload = JSON.stringify(JSON.parse(document.getElementById('insertObjectJson').value)); let req = new XMLHttpRequest(); req.addEventListener("load", updateResponseBox); - req.open('POST', `{{ protocol }}\\\\${_apiUrl.get()}\\${_corsPolicy.get()}\\object`); + req.open('POST', `{{ protocol }}:\\\\${_apiUrl.get()}\\${_corsPolicy.get()}\\object`); req.withCredentials = true; req.send(payload); }, @@ -262,7 +262,7 @@ let req = new XMLHttpRequest(); let uid = document.getElementById('getObjectUid').value; req.addEventListener("load", updateResponseBox); - req.open('GET', `{{ protocol }}\\\\${_apiUrl.get()}\\${_corsPolicy.get()}\\object\\${uid}?ts=${Date.now()}`); + req.open('GET', `{{ protocol }}:\\\\${_apiUrl.get()}\\${_corsPolicy.get()}\\object\\${uid}?ts=${Date.now()}`); req.withCredentials = true; req.send(); }, @@ -271,7 +271,7 @@ let payload = JSON.stringify(JSON.parse(document.getElementById('updateObjectJson').value)); let uid = document.getElementById('updateObjectUid').value; req.addEventListener("load", updateResponseBox); - req.open('PUT', `{{ protocol }}\\\\${_apiUrl.get()}\\${_corsPolicy.get()}\\object\\${uid}`); + req.open('PUT', `{{ protocol }}:\\\\${_apiUrl.get()}\\${_corsPolicy.get()}\\object\\${uid}`); req.withCredentials = true; req.send(payload); }, @@ -279,7 +279,7 @@ let req = new XMLHttpRequest(); let uid = document.getElementById('deleteObjectUid').value; req.addEventListener("load", updateResponseBox); - req.open('DELETE', `{{ protocol }}\\\\${_apiUrl.get()}\\${_corsPolicy.get()}\\object\\${uid}`); + req.open('DELETE', `{{ protocol }}:\\\\${_apiUrl.get()}\\${_corsPolicy.get()}\\object\\${uid}`); req.withCredentials = true; req.send(); } diff --git a/sample.env b/sample.env index 9db1ed9..44bd16d 100644 --- a/sample.env +++ b/sample.env @@ -1,5 +1,6 @@ CORS_API_PORT=3020 CORS_API_HOST=localhost:3021 +CORS_CLIENT_HOST=localhost:3021 CORS_CLIENT_PORT=3021 OAUTH_PROVIDER_PORT=3030 OAUTH_CLIENT_PORT=3031 From 5f7f92b38267f07b828edb907f3ec1e098866d19 Mon Sep 17 00:00:00 2001 From: Mic Date: Tue, 25 Aug 2020 20:39:21 -0400 Subject: [PATCH 5/9] Added configurability for CORS API host through settings --- client.cors.demo/app.js | 12 ++- client.cors.demo/static/js/fa.all.min.js | 5 ++ client.cors.demo/static/js/fontawesome.min.js | 5 ++ client.cors.demo/views/_base.njk | 34 ++++++++ client.cors.demo/views/index.njk | 54 +++++------- client.cors.demo/views/settings.njk | 84 +++++++++++++++++++ 6 files changed, 158 insertions(+), 36 deletions(-) create mode 100644 client.cors.demo/static/js/fa.all.min.js create mode 100644 client.cors.demo/static/js/fontawesome.min.js create mode 100644 client.cors.demo/views/_base.njk create mode 100644 client.cors.demo/views/settings.njk diff --git a/client.cors.demo/app.js b/client.cors.demo/app.js index fa8677d..55a471e 100644 --- a/client.cors.demo/app.js +++ b/client.cors.demo/app.js @@ -5,6 +5,12 @@ const app = express() const jucksEnv = new nunjucks.Environment(new nunjucks.FileSystemLoader('client.cors.demo/views')) const apiHost = process.env.CORS_API_HOST || 'api.cors.dem'; +const protocol = stringToBool(process.env.USE_TLS) ? 'https' : 'http'; + +// Escape backticks - for dumping variables into template literals on the front-end. +jucksEnv.addFilter("escbt", (str) => { + return str.replace(/`/g, '\\`'); +}); jucksEnv.express(app) app.set('view engine', 'njk') @@ -12,7 +18,11 @@ app.set('view engine', 'njk') app.use(express.static('client.cors.demo/static')) app.get('/', (req, res) => { - res.render('index', { apiHost: apiHost, protocol: stringToBool(process.env.USE_TLS) ? 'https' : 'http' }) + res.render('index', { apiHost: apiHost, protocol: protocol }) +}) + +app.get('/settings', (req, res) => { + res.render('settings', { apiHost: apiHost, protocol: protocol }) }) module.exports = app diff --git a/client.cors.demo/static/js/fa.all.min.js b/client.cors.demo/static/js/fa.all.min.js new file mode 100644 index 0000000..455df8c --- /dev/null +++ b/client.cors.demo/static/js/fa.all.min.js @@ -0,0 +1,5 @@ +/*! + * Font Awesome Free 5.14.0 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +!function(){"use strict";var c={},l={};try{"undefined"!=typeof window&&(c=window),"undefined"!=typeof document&&(l=document)}catch(c){}var h=(c.navigator||{}).userAgent,z=void 0===h?"":h,a=c,v=l,m=(a.document,!!v.documentElement&&!!v.head&&"function"==typeof v.addEventListener&&v.createElement,~z.indexOf("MSIE")||z.indexOf("Trident/"),"___FONT_AWESOME___"),s=function(){try{return!0}catch(c){return!1}}();var e=a||{};e[m]||(e[m]={}),e[m].styles||(e[m].styles={}),e[m].hooks||(e[m].hooks={}),e[m].shims||(e[m].shims=[]);var t=e[m];function M(c,z){var l=(2>>0;h--;)l[h]=c[h];return l}function gc(c){return c.classList?bc(c.classList):(c.getAttribute("class")||"").split(" ").filter(function(c){return c})}function Ac(c,l){var h,z=l.split("-"),a=z[0],v=z.slice(1).join("-");return a!==c||""===v||(h=v,~T.indexOf(h))?null:v}function Sc(c){return"".concat(c).replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")}function yc(h){return Object.keys(h||{}).reduce(function(c,l){return c+"".concat(l,": ").concat(h[l],";")},"")}function wc(c){return c.size!==Lc.size||c.x!==Lc.x||c.y!==Lc.y||c.rotate!==Lc.rotate||c.flipX||c.flipY}function kc(c){var l=c.transform,h=c.containerWidth,z=c.iconWidth,a={transform:"translate(".concat(h/2," 256)")},v="translate(".concat(32*l.x,", ").concat(32*l.y,") "),m="scale(".concat(l.size/16*(l.flipX?-1:1),", ").concat(l.size/16*(l.flipY?-1:1),") "),s="rotate(".concat(l.rotate," 0 0)");return{outer:a,inner:{transform:"".concat(v," ").concat(m," ").concat(s)},path:{transform:"translate(".concat(z/2*-1," -256)")}}}var Zc={x:0,y:0,width:"100%",height:"100%"};function xc(c){var l=!(1").concat(m.map(Jc).join(""),"")}var $c=function(){};function cl(c){return"string"==typeof(c.getAttribute?c.getAttribute(J):null)}var ll={replace:function(c){var l=c[0],h=c[1].map(function(c){return Jc(c)}).join("\n");if(l.parentNode&&l.outerHTML)l.outerHTML=h+($.keepOriginalSource&&"svg"!==l.tagName.toLowerCase()?"\x3c!-- ".concat(l.outerHTML," --\x3e"):"");else if(l.parentNode){var z=document.createElement("span");l.parentNode.replaceChild(z,l),z.outerHTML=h}},nest:function(c){var l=c[0],h=c[1];if(~gc(l).indexOf($.replacementClass))return ll.replace(c);var z=new RegExp("".concat($.familyPrefix,"-.*"));delete h[0].attributes.style,delete h[0].attributes.id;var a=h[0].attributes.class.split(" ").reduce(function(c,l){return l===$.replacementClass||l.match(z)?c.toSvg.push(l):c.toNode.push(l),c},{toNode:[],toSvg:[]});h[0].attributes.class=a.toSvg.join(" ");var v=h.map(function(c){return Jc(c)}).join("\n");l.setAttribute("class",a.toNode.join(" ")),l.setAttribute(J,""),l.innerHTML=v}};function hl(c){c()}function zl(h,c){var z="function"==typeof c?c:$c;if(0===h.length)z();else{var l=hl;$.mutateApproach===y&&(l=i.requestAnimationFrame||hl),l(function(){var c=!0===$.autoReplaceSvg?ll.replace:ll[$.autoReplaceSvg]||ll.replace,l=_c.begin("mutate");h.map(c),l(),z()})}}var al=!1;function vl(){al=!1}var ml=null;function sl(c){if(t&&$.observeMutations){var a=c.treeCallback,v=c.nodeCallback,m=c.pseudoElementsCallback,l=c.observeMutationsRoot,h=void 0===l?o:l;ml=new t(function(c){al||bc(c).forEach(function(c){if("childList"===c.type&&0>>0;n--;)e[n]=t[n];return e}function Ct(t){return t.classList?At(t.classList):(t.getAttribute("class")||"").split(" ").filter(function(t){return t})}function Ot(t,e){var n,a=e.split("-"),r=a[0],i=a.slice(1).join("-");return r!==t||""===i||(n=i,~F.indexOf(n))?null:i}function St(t){return"".concat(t).replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")}function Pt(n){return Object.keys(n||{}).reduce(function(t,e){return t+"".concat(e,": ").concat(n[e],";")},"")}function Nt(t){return t.size!==yt.size||t.x!==yt.x||t.y!==yt.y||t.rotate!==yt.rotate||t.flipX||t.flipY}function Mt(t){var e=t.transform,n=t.containerWidth,a=t.iconWidth,r={transform:"translate(".concat(n/2," 256)")},i="translate(".concat(32*e.x,", ").concat(32*e.y,") "),o="scale(".concat(e.size/16*(e.flipX?-1:1),", ").concat(e.size/16*(e.flipY?-1:1),") "),c="rotate(".concat(e.rotate," 0 0)");return{outer:r,inner:{transform:"".concat(i," ").concat(o," ").concat(c)},path:{transform:"translate(".concat(a/2*-1," -256)")}}}var zt={x:0,y:0,width:"100%",height:"100%"};function Et(t){var e=!(1").concat(o.map(Zt).join(""),"")}var $t=function(){};function te(t){return"string"==typeof(t.getAttribute?t.getAttribute(Z):null)}var ee={replace:function(t){var e=t[0],n=t[1].map(function(t){return Zt(t)}).join("\n");if(e.parentNode&&e.outerHTML)e.outerHTML=n+($.keepOriginalSource&&"svg"!==e.tagName.toLowerCase()?"\x3c!-- ".concat(e.outerHTML," --\x3e"):"");else if(e.parentNode){var a=document.createElement("span");e.parentNode.replaceChild(a,e),a.outerHTML=n}},nest:function(t){var e=t[0],n=t[1];if(~Ct(e).indexOf($.replacementClass))return ee.replace(t);var a=new RegExp("".concat($.familyPrefix,"-.*"));delete n[0].attributes.style,delete n[0].attributes.id;var r=n[0].attributes.class.split(" ").reduce(function(t,e){return e===$.replacementClass||e.match(a)?t.toSvg.push(e):t.toNode.push(e),t},{toNode:[],toSvg:[]});n[0].attributes.class=r.toSvg.join(" ");var i=n.map(function(t){return Zt(t)}).join("\n");e.setAttribute("class",r.toNode.join(" ")),e.setAttribute(Z,""),e.innerHTML=i}};function ne(t){t()}function ae(n,t){var a="function"==typeof t?t:$t;if(0===n.length)a();else{var e=ne;$.mutateApproach===P&&(e=g.requestAnimationFrame||ne),e(function(){var t=!0===$.autoReplaceSvg?ee.replace:ee[$.autoReplaceSvg]||ee.replace,e=Yt.begin("mutate");n.map(t),e(),a()})}}var re=!1;function ie(){re=!1}var oe=null;function ce(t){if(l&&$.observeMutations){var r=t.treeCallback,i=t.nodeCallback,o=t.pseudoElementsCallback,e=t.observeMutationsRoot,n=void 0===e?v:e;oe=new l(function(t){re||At(t).forEach(function(t){if("childList"===t.type&&0 + + +CORS Demonstrator{% block pageTitle %}{% endblock %} + + + + +{% block body %}{% endblock %} + + + \ No newline at end of file diff --git a/client.cors.demo/views/index.njk b/client.cors.demo/views/index.njk index 8910c93..44bb963 100644 --- a/client.cors.demo/views/index.njk +++ b/client.cors.demo/views/index.njk @@ -1,18 +1,11 @@ - - +{% extends "_base.njk" %} - - CORS Demo - - - - - +{% block body %}

- CORS + Musashi.js - CORS

Cross-Origin Resource Sharing - Demonstation @@ -27,12 +20,12 @@
- +

- +

@@ -45,7 +38,7 @@
  • - Same-Origin + No CORS
  • @@ -203,12 +196,9 @@ } })(); let _apiUrl = (function() { - let textbox = document.getElementById('apiUrlTextbox'); - let currentApiUrl = '{{ apiHost }}'; - textbox.value = currentApiUrl; + let currentApiUrl = `{{ protocol | escbt }}://{{ apiHost | escbt }}`; return { set: function(val) { - textbox.value = val; currentApiUrl = val; }, get: function() { @@ -238,7 +228,7 @@ let req = new XMLHttpRequest(); req.addEventListener("load", updateResponseBox); - req.open('POST', `{{ protocol }}:\\\\${_apiUrl.get()}\\auth\\login`); + req.open('POST', `${_apiUrl.get()}\\auth\\login`); req.withCredentials = true; req.send(JSON.stringify(body)); }, @@ -246,7 +236,7 @@ let req = new XMLHttpRequest(); req.addEventListener("load", updateResponseBox); req.addEventListener("error", showRequestError); - req.open('GET', `{{ protocol }}:\\\\${_apiUrl.get()}\\${_corsPolicy.get()}\\object?ts=${Date.now()}`); + req.open('GET', `${_apiUrl.get()}\\${_corsPolicy.get()}\\object?ts=${Date.now()}`); req.withCredentials = true; req.send(); }, @@ -254,7 +244,7 @@ let payload = JSON.stringify(JSON.parse(document.getElementById('insertObjectJson').value)); let req = new XMLHttpRequest(); req.addEventListener("load", updateResponseBox); - req.open('POST', `{{ protocol }}:\\\\${_apiUrl.get()}\\${_corsPolicy.get()}\\object`); + req.open('POST', `${_apiUrl.get()}\\${_corsPolicy.get()}\\object`); req.withCredentials = true; req.send(payload); }, @@ -262,7 +252,7 @@ let req = new XMLHttpRequest(); let uid = document.getElementById('getObjectUid').value; req.addEventListener("load", updateResponseBox); - req.open('GET', `{{ protocol }}:\\\\${_apiUrl.get()}\\${_corsPolicy.get()}\\object\\${uid}?ts=${Date.now()}`); + req.open('GET', `${_apiUrl.get()}\\${_corsPolicy.get()}\\object\\${uid}?ts=${Date.now()}`); req.withCredentials = true; req.send(); }, @@ -271,7 +261,7 @@ let payload = JSON.stringify(JSON.parse(document.getElementById('updateObjectJson').value)); let uid = document.getElementById('updateObjectUid').value; req.addEventListener("load", updateResponseBox); - req.open('PUT', `{{ protocol }}:\\\\${_apiUrl.get()}\\${_corsPolicy.get()}\\object\\${uid}`); + req.open('PUT', `${_apiUrl.get()}\\${_corsPolicy.get()}\\object\\${uid}`); req.withCredentials = true; req.send(payload); }, @@ -279,7 +269,7 @@ let req = new XMLHttpRequest(); let uid = document.getElementById('deleteObjectUid').value; req.addEventListener("load", updateResponseBox); - req.open('DELETE', `{{ protocol }}:\\\\${_apiUrl.get()}\\${_corsPolicy.get()}\\object\\${uid}`); + req.open('DELETE', `${_apiUrl.get()}\\${_corsPolicy.get()}\\object\\${uid}`); req.withCredentials = true; req.send(); } @@ -293,11 +283,6 @@ let tabGroup = document.getElementsByName('corsTabs'); - document.getElementById('apiUrlTextbox').addEventListener('blur', (event) => { - _apiUrl.set(event.target.value); - alert(`Updated API server to ${event.target.value}`); - }); - corsTabRegex.addEventListener('click', (function(tab, group) { return function() { Array.from(group).map((item) => { @@ -328,13 +313,12 @@ } })(corsTabReflected, tabGroup)); - if(document.location.search.length > 1) { - let queryString = document.location.search.substring(1).split('&').reduce((ars, ar) => { let ar1 = ar.split('='); ars[ar1[0]] = ar1[1]; return ars}, {}); - if(queryString.apiUrl) { - _apiUrl.set(queryString.apiUrl); - } + let sessionApiUrl = sessionStorage.getItem('apiHost'); + if (sessionApiUrl !== null) { + _apiUrl.set(sessionApiUrl); + document.getElementById('apiUrlTextbox').value = sessionApiUrl; } + }); - - +{% endblock %} \ No newline at end of file diff --git a/client.cors.demo/views/settings.njk b/client.cors.demo/views/settings.njk new file mode 100644 index 0000000..bc4adb6 --- /dev/null +++ b/client.cors.demo/views/settings.njk @@ -0,0 +1,84 @@ +{% extends "_base.njk" %} + +{% block body %} + +
    +
    +
    +

    + Musashi.js - CORS +

    +

    + Settings +

    +
    +
    +
    +
    + +
    + + + +{% endblock %} \ No newline at end of file From 58b128709f85c3732037b71ec1f8a8c133683819 Mon Sep 17 00:00:00 2001 From: Mic Date: Wed, 26 Aug 2020 17:14:42 -0400 Subject: [PATCH 6/9] Wrote CORS ex1, setup exercise framework --- client.cors.demo/app.js | 8 ++ client.cors.demo/views/exercises/_ex.njk | 82 +++++++++++++++++++ client.cors.demo/views/exercises/ex1.njk | 15 ++++ client.cors.demo/views/exercises/ex2.njk | 3 + .../views/exercises/sample_ex.njk | 21 +++++ 5 files changed, 129 insertions(+) create mode 100644 client.cors.demo/views/exercises/_ex.njk create mode 100644 client.cors.demo/views/exercises/ex1.njk create mode 100644 client.cors.demo/views/exercises/ex2.njk create mode 100644 client.cors.demo/views/exercises/sample_ex.njk diff --git a/client.cors.demo/app.js b/client.cors.demo/app.js index 55a471e..18185ec 100644 --- a/client.cors.demo/app.js +++ b/client.cors.demo/app.js @@ -25,6 +25,14 @@ app.get('/settings', (req, res) => { res.render('settings', { apiHost: apiHost, protocol: protocol }) }) +app.get('/ex/:exnum', (req, res) => { + res.render(`exercises/ex${req.params.exnum}`, { apiHost: apiHost, protocol: protocol, exnum: req.params.exnum, showSolution: false }) +}) + +app.post('/ex/:exnum', (req, res) => { + res.render(`exercises/ex${req.params.exnum}`, { apiHost: apiHost, protocol: protocol, exnum: req.params.exnum, corsClientHost: process.env.CORS_CLIENT_HOST, showSolution: true }) +}) + module.exports = app function stringToBool(str) { diff --git a/client.cors.demo/views/exercises/_ex.njk b/client.cors.demo/views/exercises/_ex.njk new file mode 100644 index 0000000..2c1dfe6 --- /dev/null +++ b/client.cors.demo/views/exercises/_ex.njk @@ -0,0 +1,82 @@ +{% extends "_base.njk" %} + +{% block pageTitle %} - Ex{{ exnum }}{% endblock %} + +{% block body %} + + + +
    +
    + {% block instructions %} +These exercises are performed by tampering with the request in your interception proxy (e.g. Burp or ZAP). Be sure to read the scenario description before for critical +details, including the setup information for your starting point, and possible hints. Also, make note of the goal, as not every exercise has the same success criteria. + {% endblock %} +
    +
    + +
    +
    +

    + Scenario +

    +

    {% block scenario %}{% endblock %}

    +
    +
    + +
    +
    +

    + Goal +

    +

    {% block goal %}{% endblock %}

    +
    +
    + +
    +
    +
    +
    + + + {% if showSolution %} +
    +
    +

    Solution

    +

    {% block solution %}{% endblock %}

    +
    +
    + {% else %} +
    +
    +
    +
    + +
    +
    +
    +
    + {% endif %} +

+ + {% block sampleFunction %} + + {% endblock %} + + +{% endblock %} \ No newline at end of file diff --git a/client.cors.demo/views/exercises/ex1.njk b/client.cors.demo/views/exercises/ex1.njk new file mode 100644 index 0000000..8c627d1 --- /dev/null +++ b/client.cors.demo/views/exercises/ex1.njk @@ -0,0 +1,15 @@ +{% extends "exercises/_ex.njk" %} + +{% block scenario %} +This API endpoint is using a regular expression to limit the calls to only those from the origin of {{ corsClientHost }}, and subdomains. +However, there's a flaw in the implementation of this pattern. +{% endblock %} + +{% block goal %} +Find an origin is allowed by the Access-Control-Allow-Origin response header, even though it doesn't belong to this domain. +{% endblock %} + +{% block solution %} +Any origin ending in {{ corsClientHost }} is allowed, even https://evil{{ corsClientHost }}. +To fix this, the pattern should require a dot or period . character before the domain or hostname, unless it is immediately preceded by the protocol. +{% endblock %} \ No newline at end of file diff --git a/client.cors.demo/views/exercises/ex2.njk b/client.cors.demo/views/exercises/ex2.njk new file mode 100644 index 0000000..e3db70f --- /dev/null +++ b/client.cors.demo/views/exercises/ex2.njk @@ -0,0 +1,3 @@ +{% extends "exercises/_ex.njk" %} + +{% block scenario %}Foo{% endblock %} \ No newline at end of file diff --git a/client.cors.demo/views/exercises/sample_ex.njk b/client.cors.demo/views/exercises/sample_ex.njk new file mode 100644 index 0000000..ebbc605 --- /dev/null +++ b/client.cors.demo/views/exercises/sample_ex.njk @@ -0,0 +1,21 @@ +{% extends "exercises/_ex.njk" %} + +{% block scenario %} +Describe the scenario +{% endblock %} + +{% block goal %} +Indicate the success condition +{% endblock %} + +{% block solution %} +This is the solution +{% endblock %} + +{% block sampleFunction %} + +{% endblock %} \ No newline at end of file From 4d1eb5dc8ea892263198467689a4c630d78c9437 Mon Sep 17 00:00:00 2001 From: Mic Date: Wed, 26 Aug 2020 21:37:41 -0400 Subject: [PATCH 7/9] Completed CORS ex1 and ex2 --- api.cors.demo/app.js | 1 + api.cors.demo/controllers/dummy.js | 6 ++++ api.cors.demo/routes/ex.js | 44 ++++++++++++++++++++++++ api.cors.demo/routes/pattern.js | 2 +- client.cors.demo/views/exercises/_ex.njk | 2 +- client.cors.demo/views/exercises/ex1.njk | 2 +- client.cors.demo/views/exercises/ex2.njk | 13 ++++++- client.cors.demo/views/index.njk | 2 +- client.cors.demo/views/settings.njk | 2 +- 9 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 api.cors.demo/controllers/dummy.js create mode 100644 api.cors.demo/routes/ex.js diff --git a/api.cors.demo/app.js b/api.cors.demo/app.js index 15b914d..4a4b480 100644 --- a/api.cors.demo/app.js +++ b/api.cors.demo/app.js @@ -11,5 +11,6 @@ api.use('/auth', require('./routes/auth')) api.use('/sop', require('./routes/sop')) api.use('/pattern', require('.//routes/pattern')) api.use('/reflect', require('./routes/reflect')) +api.use('/ex', require('./routes/ex')) module.exports = api diff --git a/api.cors.demo/controllers/dummy.js b/api.cors.demo/controllers/dummy.js new file mode 100644 index 0000000..6d1ff68 --- /dev/null +++ b/api.cors.demo/controllers/dummy.js @@ -0,0 +1,6 @@ +exports.getData = (req, res) => { + res.json({ + theData: [{ name: 'osborn', class: 'monk'}, { name: 'dookie', class: 'barbarian'}, { name: 'fix', class: 'bard' }, { name: 'dugros', class: 'fighter'}], + result: 'critical success' + }); +} \ No newline at end of file diff --git a/api.cors.demo/routes/ex.js b/api.cors.demo/routes/ex.js new file mode 100644 index 0000000..6761720 --- /dev/null +++ b/api.cors.demo/routes/ex.js @@ -0,0 +1,44 @@ +const express = require('express') + +const router = express.Router(); + +const dummyController = require('../controllers/dummy') + +const cors = require('cors') + +function escapeRegex(string) { + return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); +} + +// ex1 - missing start anchor +const ex1router = express.Router({ mergeParams: true }) + +const ex1Policy = { + origin: new RegExp(escapeRegex(process.env.CORS_CLIENT_HOST) + '$'), + methods: 'GET,POST', + credentials: true +} + +ex1router.use(cors(ex1Policy)) +ex1router.options('*', cors(ex1Policy)) + +ex1router.get('/1', dummyController.getData) + +// ex2 - missing end anchor +const ex2router = express.Router({ mergeParams: true }) + +const ex2Policy = { + origin: new RegExp('^https?:\\/\\/(www\\.|blog\\.)?professionallyevil.com'), + methods: 'GET,POST', + credentials: true +} + +ex2router.use(cors(ex2Policy)) +ex2router.options('*', cors(ex2Policy)) + +ex2router.get('/2', dummyController.getData) + +router.use(ex1router) +router.use(ex2router) + +module.exports = router \ No newline at end of file diff --git a/api.cors.demo/routes/pattern.js b/api.cors.demo/routes/pattern.js index 3212650..471bac8 100644 --- a/api.cors.demo/routes/pattern.js +++ b/api.cors.demo/routes/pattern.js @@ -13,7 +13,7 @@ function escapeRegex(string) { } const corsOptions = { - origin: new RegExp(escapeRegex(process.env.CORS_CLIENT_HOST) + '$'), + origin: new RegExp('https?:\\/\\/([0-9a-z\\.\\-]+\\.)?' + escapeRegex(process.env.CORS_CLIENT_HOST) + '$'), methods: 'GET,POST,PUT,DELETE', credentials: true } diff --git a/client.cors.demo/views/exercises/_ex.njk b/client.cors.demo/views/exercises/_ex.njk index 2c1dfe6..b05a768 100644 --- a/client.cors.demo/views/exercises/_ex.njk +++ b/client.cors.demo/views/exercises/_ex.njk @@ -21,7 +21,7 @@
{% block instructions %} These exercises are performed by tampering with the request in your interception proxy (e.g. Burp or ZAP). Be sure to read the scenario description before for critical -details, including the setup information for your starting point, and possible hints. Also, make note of the goal, as not every exercise has the same success criteria. +details, including the setup information for your starting point - including which Origins are known to be allowed, as well as possible hints. Also, make note of the goal, as not every exercise has the same success criteria. {% endblock %}
diff --git a/client.cors.demo/views/exercises/ex1.njk b/client.cors.demo/views/exercises/ex1.njk index 8c627d1..25a9786 100644 --- a/client.cors.demo/views/exercises/ex1.njk +++ b/client.cors.demo/views/exercises/ex1.njk @@ -1,7 +1,7 @@ {% extends "exercises/_ex.njk" %} {% block scenario %} -This API endpoint is using a regular expression to limit the calls to only those from the origin of {{ corsClientHost }}, and subdomains. +This API endpoint is using a regular expression to limit the calls to only those from the origin of {{ corsClientHost }}, and subdomains such as staging.{{ corsClientHost }}. However, there's a flaw in the implementation of this pattern. {% endblock %} diff --git a/client.cors.demo/views/exercises/ex2.njk b/client.cors.demo/views/exercises/ex2.njk index e3db70f..1f23de1 100644 --- a/client.cors.demo/views/exercises/ex2.njk +++ b/client.cors.demo/views/exercises/ex2.njk @@ -1,3 +1,14 @@ {% extends "exercises/_ex.njk" %} -{% block scenario %}Foo{% endblock %} \ No newline at end of file +{% block scenario %} +A regular expression is used to limit the allowed origins to just professionallyevil.com, www.professionallyevil.com, and blog.professionallyevil.com. +There is a flaw in this regular expression. +{% endblock %} + +{% block goal %} +Find an origin is allowed by the Access-Control-Allow-Origin response header, even though it doesn't belong to this domain. +{% endblock %} + +{% block solution %} +This regular expression is missing the end anchor $, so any origin starting with an allowed domain will work, including https://professionallyevil.com.{{ corsClientHost }}. +{% endblock %} \ No newline at end of file diff --git a/client.cors.demo/views/index.njk b/client.cors.demo/views/index.njk index 44bb963..7b84e2a 100644 --- a/client.cors.demo/views/index.njk +++ b/client.cors.demo/views/index.njk @@ -47,7 +47,7 @@
  • - + Regex
  • diff --git a/client.cors.demo/views/settings.njk b/client.cors.demo/views/settings.njk index bc4adb6..e51e548 100644 --- a/client.cors.demo/views/settings.njk +++ b/client.cors.demo/views/settings.njk @@ -28,7 +28,7 @@
    - +

    Note this only affects the Home page. Exercises will be unaffected.

    From f5702657d6847887f055bc0cb072a3994c9716c6 Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 27 Aug 2020 09:05:52 -0400 Subject: [PATCH 8/9] Project branding on CSP --- csp.demo/views/index.njk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csp.demo/views/index.njk b/csp.demo/views/index.njk index 5c17f57..2794da4 100644 --- a/csp.demo/views/index.njk +++ b/csp.demo/views/index.njk @@ -5,7 +5,7 @@

    - Hello CSP Demonstrator! + Musashi.js - CSP

    Use the options below to test injection payloads. Note that your CSP can affect the JS and CSS on this page. From 304840eadb3b27f507b6e209846f4e639cb2fd30 Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 27 Aug 2020 09:43:21 -0400 Subject: [PATCH 9/9] Updated README to reflect 2.0 changes --- README.md | 53 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ce0f143..306ddb9 100644 --- a/README.md +++ b/README.md @@ -3,27 +3,62 @@ Musashi.js is a set of Node applications for demonstrating web security concepts. Created for use in Samurai WTF. -## Status of the Applications - - CORS Demonstrator - Ready for general use - - CSP Demonstrator - Beta - - OAuth Demonstrator - Not ready for use, WIP +## Applications Ready for General Use + - CORS Demonstrator + - CSP Demonstrator + + ## Unusable Applications (Work-in-Progress or Roadmap) + - OAuth Demonstrator + - Sandbox for CSRF, CORS, XSS exercises + - Help page ## Starting the services You need Node and Yarn installed an in the path. 1. Clone this repo 2. `yarn install` - 3. `yarn start` + 3. Create a `.env` that's appropriate to your environment. The [sample.env](sample.env) file is available as a reference. Detailed further in the following section. + 4. `yarn start` + +## Customizing your .env +There are a handful of settings in the `.env` file. Here's what they are and what they do: + - **CORS_API_PORT** (default: `3020`) - Port to bind to for the CORS Demonstrator API + - **CORS_API_HOST** (default: `localhost:3020`) - Hostname for the CORS Demonstrator API, used to populate defaults in the CORS demo client + - **CORS_CLIENT_HOST** (default: `localhost:3021`) - Hostname for the CORS demonstrator client, used to dynamically generate Regex-based CORS policies + - **CORS_CLIENT_PORT** (default: `3021`) - Port to bind to for the CORS client + - **OAUTH_PROVIDER_PORT** (default: `3030`) - Port to bind to for the OAuth Identity Provider *(Currently disabled)* + - **OAUTH_CLIENT_PORT** (default: `3031`) - Port to bind to for the OAuth Client app *(Currently disabled)* + - **CSP_APP_PORT** (default: `3041`) - Port to bind to for the Content Security Policy demo app + - **USE_TLS** (default: `FALSE`) - Affects the protocol used in the CORS demonstrator to call the API. `TRUE` for **https**, `FALSE` for **http**. *This does not actually enable TLS on the listener at this time. It's useful if going through a reverse-proxy with TLS enabled. In a future release, it will be required that this be TRUE. This is due to coming changes in standard browser behavior around cookies.* -Console output will describe which servers are listening on which ports. To override these, add a `.env` file with your own port specifications. The [sample.env](sample.env) file is available as a reference. +Here's a default local dev configuration: +``` +CORS_API_PORT=3020 +CORS_API_HOST=localhost:3020 +CORS_CLIENT_HOST=localhost:3021 +CORS_CLIENT_PORT=3021 +OAUTH_PROVIDER_PORT=3030 +OAUTH_CLIENT_PORT=3031 +CSP_APP_PORT=3041 +USE_TLS=FALSE +``` ## CORS Demonstrator ### Usage 1. Open the CORS Client app, which is on localhost:3021 by default. - 2. Set the API URL textbox to the actual hostname/port for your API. If you're not using a reverse proxy or hostname resolution, localhost:3020 would be the right default value here. - 3. The policy selector on the top right lets you set which CORS policy you're reaching the on the server. *_NB: The **Pattern** option uses a hardcoded regex for cors.dem, missing the front anchor. If you don't have a compliant hostname set for the client (e.g. client.cors.dem), you will likely want to tamper with the Origin header using a MITM proxy to demonstrate this._* - 4. Down the left side are a variety of request types. The Auth one will take any set of credentials and will set a cookie. It is *never* blocked by a CORS policy. The other request types all require an auth cookie. + 2. The API URL box will indicate the actual hostname/port that will be targeted for your API. If you're not using a reverse proxy or hostname resolution, localhost:3020 would be the right default value here. This value can be modified in the *Settings* page if necessary, although only the home page will be affected. Typically if this is incorrect, it should be corrected in the `.env` which will necessitate restarting the application. + 3. The policy selector on the top right lets you set which CORS policy you're reaching the on the server. The Regex option is dynamically generated based on the **CORS_CLIENT_HOST** supplied in the `.env` file. It allows that Origin, and subdomains of that Origin. + 4. Down the left side of the *Home* page are a variety of request types. The Auth one will take any set of credentials and will set a cookie. It is *never* blocked by a CORS policy. The other request types all require an auth cookie. + 5. The *Exercises* each provide a scenario, a goal (success condition), and the ability to generate a sample request. Note that the `Origin` header in the sample request may not be an allowed Origin in the context of the exercise. The scenario will explain what the intended behavior is. Exercises are completed by modifying the request in your interception proxy until the goal is met. There is no automatic detection of a success, it is up to the student to determine based on the response if they have met the goal. ### Additional notes - Some of the HTTP Methods used will always trigger a CORS preflight (e.g. PUT and DELETE) - When set to Same-Origin (no CORS policy), the CORS middleware isn't used at all, and therefore preflights will get an Unauthorized response. + + + ## CSP Demonstrator + ### Usage + 1. Open the CSP app, which is localhost:3041 by default. This should match the port specified in your `.env`. + 2. The home page provides the ability to execute XSS-style JavaScript payloads in through both reflected and DOM-based interactions. There is no filtering on these. + 3. The *Set CSP* page allows you to set a custom content-security-policy. This applies across the application, except on the *Set CSP* page itself. It may not have every directive, but the all of the common ones and some of the uncommon ones are included. Including the string `$nonce` in any of the directives will have it replaced with an actual generated nonce at dynamically when the policy header is served. + 4. Each of the *Execises* provides a CSP bypass or evasion challenge. They each have a button that replaces the application's CSP with the challenge CSP. They also have directions explaining the success condition for the exercise.