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. 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 f562f6b..471bac8 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('https?:\\/\\/([0-9a-z\\.\\-]+\\.)?' + 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/app.js b/client.cors.demo/app.js index 13956e9..18185ec 100644 --- a/client.cors.demo/app.js +++ b/client.cors.demo/app.js @@ -1,6 +1,48 @@ 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 +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') + +app.use(express.static('client.cors.demo/static')) + +app.get('/', (req, res) => { + res.render('index', { apiHost: apiHost, protocol: protocol }) +}) + +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) { + 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/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/exercises/_ex.njk b/client.cors.demo/views/exercises/_ex.njk new file mode 100644 index 0000000..b05a768 --- /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 - 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 %} +
+
+ +
+
+

+ 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..25a9786 --- /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 such as staging.{{ corsClientHost }}. +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..1f23de1 --- /dev/null +++ b/client.cors.demo/views/exercises/ex2.njk @@ -0,0 +1,14 @@ +{% extends "exercises/_ex.njk" %} + +{% 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/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 diff --git a/client.cors.demo/static/index.html b/client.cors.demo/views/index.njk similarity index 86% rename from client.cors.demo/static/index.html rename to client.cors.demo/views/index.njk index 14c648e..7b84e2a 100644 --- a/client.cors.demo/static/index.html +++ 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
  • @@ -54,7 +47,7 @@

  • - + Regex
  • @@ -203,12 +196,9 @@

    Response

    } })(); let _apiUrl = (function() { - let textbox = document.getElementById('apiUrlTextbox'); - let currentApiUrl = 'api.cors.dem'; - textbox.value = currentApiUrl; + let currentApiUrl = `{{ protocol | escbt }}://{{ apiHost | escbt }}`; return { set: function(val) { - textbox.value = val; currentApiUrl = val; }, get: function() { @@ -238,7 +228,7 @@

    Response

    let req = new XMLHttpRequest(); req.addEventListener("load", updateResponseBox); - req.open('POST', `http:\\\\${_apiUrl.get()}\\auth\\login`); + req.open('POST', `${_apiUrl.get()}\\auth\\login`); req.withCredentials = true; req.send(JSON.stringify(body)); }, @@ -246,7 +236,7 @@

    Response

    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', `${_apiUrl.get()}\\${_corsPolicy.get()}\\object?ts=${Date.now()}`); req.withCredentials = true; req.send(); }, @@ -254,7 +244,7 @@

    Response

    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', `${_apiUrl.get()}\\${_corsPolicy.get()}\\object`); req.withCredentials = true; req.send(payload); }, @@ -262,7 +252,7 @@

    Response

    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', `${_apiUrl.get()}\\${_corsPolicy.get()}\\object\\${uid}?ts=${Date.now()}`); req.withCredentials = true; req.send(); }, @@ -271,7 +261,7 @@

    Response

    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', `${_apiUrl.get()}\\${_corsPolicy.get()}\\object\\${uid}`); req.withCredentials = true; req.send(payload); }, @@ -279,7 +269,7 @@

    Response

    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', `${_apiUrl.get()}\\${_corsPolicy.get()}\\object\\${uid}`); req.withCredentials = true; req.send(); } @@ -293,11 +283,6 @@

    Response

    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 @@

    Response

    } })(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..e51e548 --- /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 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 97% rename from client.csp.demo/views/index.njk rename to csp.demo/views/index.njk index 5c17f57..2794da4 100644 --- a/client.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. 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..44bd16d 100644 --- a/sample.env +++ b/sample.env @@ -1,6 +1,8 @@ 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 -CSP_SERVER_PORT=3040 -CSP_CLIENT_PORT=3041 \ No newline at end of file +CSP_APP_PORT=3041 +USE_TLS=TRUE \ No newline at end of file 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