Skip to content

Commit

Permalink
Merge pull request #4 from SamuraiWTF/cors-2.0
Browse files Browse the repository at this point in the history
Cors 2.0
  • Loading branch information
mgillam authored Aug 27, 2020
2 parents c321e24 + 304840e commit f0a5b4a
Show file tree
Hide file tree
Showing 32 changed files with 437 additions and 70 deletions.
53 changes: 44 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

1 change: 1 addition & 0 deletions api.cors.demo/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions api.cors.demo/controllers/dummy.js
Original file line number Diff line number Diff line change
@@ -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'
});
}
44 changes: 44 additions & 0 deletions api.cors.demo/routes/ex.js
Original file line number Diff line number Diff line change
@@ -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
7 changes: 6 additions & 1 deletion api.cors.demo/routes/pattern.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
48 changes: 45 additions & 3 deletions client.cors.demo/app.js
Original file line number Diff line number Diff line change
@@ -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
}
}
5 changes: 5 additions & 0 deletions client.cors.demo/static/js/fa.all.min.js

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions client.cors.demo/static/js/fontawesome.min.js

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions client.cors.demo/views/_base.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>
<head>
<title>CORS Demonstrator{% block pageTitle %}{% endblock %}</title>
<link rel="stylesheet" href="/css/bulma.min.css" />
</head>
<body>
<nav class="navbar is-dark" role="navigation" aria-label="main navigation">
<div class="navbar-brand is-primary">
<div class="navbar-item">
CORS Demo
</div>
</div>
<div class="navbar-start">
<a href="/" class="navbar-item">Home</a>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">Exercises</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="/ex/1">
Exercise 1
</a>
<a class="navbar-item" href="/ex/2">
Exercise 2
</a>
</div>
</div>
<div class="navbar-end">
<a href="/settings" class="navbar-item">Settings</a>
</div>
</nav>
{% block body %}{% endblock %}
<script defer src="/js/fa.all.min.js"></script>
</body>
</html>
82 changes: 82 additions & 0 deletions client.cors.demo/views/exercises/_ex.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{% extends "_base.njk" %}

{% block pageTitle %} - Ex{{ exnum }}{% endblock %}

{% block body %}

<section class="hero is-link is-bold">
<div class="hero-body">
<div class="container">
<h1 class="title">
Musashi.js - CORS
</h1>
<h2 class="subtitle">
Exercise {{ exnum }}
</h2>
</div>
</div>
</section>

<section class="section has-background-light">
<div class="container">
{% 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 %}
</div>
</section>

<section class="section">
<div class="container">
<h2 class="subtitle">
Scenario
</h2>
<p>{% block scenario %}{% endblock %}</p>
</div>
</section>

<section class="section">
<div class="container">
<h2 class="subtitle">
Goal
</h2>
<p>{% block goal %}{% endblock %}</p>
<div class="field">
<div class="control">
<button class="button is-info is-rounded" onclick="sendSampleRequest(`{{ protocol | escbt }}://{{ apiHost | escbt }}/ex/{{ exnum | escbt }}`);alert('Sample request has been sent.');">Send Request</button>
</div>
</div>
</div>
</section>


{% if showSolution %}
<section class="hero is-warning">
<div class="container">
<h2 class="subtitle">Solution</h2>
<p>{% block solution %}{% endblock %}</p>
</div>
</section>
{% else %}
<section class="section">
<form class="container" method="POST" action="/ex/{{ exnum }}">
<div class="field">
<div class="control">
<button class="button is-danger is-rounded" type="submit">Show Solution</button>
</div>
</div>
</form>
</section>
{% endif %}
</section>

{% block sampleFunction %}
<script>
function sendSampleRequest(targetUrl) {
fetch(targetUrl);
}
</script>
{% endblock %}


{% endblock %}
15 changes: 15 additions & 0 deletions client.cors.demo/views/exercises/ex1.njk
Original file line number Diff line number Diff line change
@@ -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 <i>{{ corsClientHost }}</i>, and subdomains such as <i>staging.{{ corsClientHost }}</i>.
However, there's a flaw in the implementation of this pattern.
{% endblock %}

{% block goal %}
Find an origin is allowed by the <code>Access-Control-Allow-Origin</code> response header, even though it doesn't belong to this domain.
{% endblock %}

{% block solution %}
Any origin ending in <i>{{ corsClientHost }}</i> is allowed, even <b>https://evil{{ corsClientHost }}</b>.
To fix this, the pattern should require a dot or period <code>.</code> character before the domain or hostname, unless it is immediately preceded by the protocol.
{% endblock %}
14 changes: 14 additions & 0 deletions client.cors.demo/views/exercises/ex2.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{% extends "exercises/_ex.njk" %}

{% block scenario %}
A regular expression is used to limit the allowed origins to just <i>professionallyevil.com</i>, <i>www.professionallyevil.com</i>, and <i>blog.professionallyevil.com</i>.
There is a flaw in this regular expression.
{% endblock %}

{% block goal %}
Find an origin is allowed by the <code>Access-Control-Allow-Origin</code> response header, even though it doesn't belong to this domain.
{% endblock %}

{% block solution %}
This regular expression is missing the end anchor <code>$</code>, so any origin <u>starting</u> with an allowed domain will work, including <b>https://professionallyevil.com.{{ corsClientHost }}</b>.
{% endblock %}
21 changes: 21 additions & 0 deletions client.cors.demo/views/exercises/sample_ex.njk
Original file line number Diff line number Diff line change
@@ -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 %}
<script>
function sendSampleRequest(targetUrl) {
// write this for a custom request. Delete the whole block if you just want a fetch to the /ex/:num endpoint.
}
</script>
{% endblock %}
Loading

0 comments on commit f0a5b4a

Please sign in to comment.