├── .env.example ├── lib ├── home │ ├── index.js │ └── home.marko ├── shared │ ├── utils.js │ ├── https.js │ ├── jwt.js │ ├── layout.marko │ └── db.js ├── register │ └── index.js ├── csp-report │ └── index.js ├── login │ ├── login.marko │ └── index.js ├── post │ └── index.js └── index.js ├── public ├── images │ ├── background-pattern.svg │ └── github.svg ├── styles │ └── main.css └── scripts │ └── post.js ├── package.json ├── LICENSE ├── .gitignore ├── docs ├── vulnerabilities │ ├── window-redirection.md │ ├── cross-site-request-forgery.md │ ├── cross-site-scripting.md │ ├── clickjacking.md │ ├── jsonp.md │ └── json-web-token.md └── security │ └── content-security-policy.md └── README.md /.env.example: -------------------------------------------------------------------------------- 1 | TWILIO_ACCOUNT_SID= 2 | TWILIO_API_KEY= 3 | TWILIO_API_SECRET= 4 | TWILIO_SYNC_SERVICE= -------------------------------------------------------------------------------- /lib/home/index.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { getPosts } = require('../shared/db'); 3 | const { csrfIfSecure } = require('../shared/utils'); 4 | 5 | router.get('/', csrfIfSecure, async (req, res) => { 6 | const tmpl = require('./home.marko'); 7 | const posts = await getPosts(); 8 | res.marko(tmpl, { ...req.user, posts }); 9 | }); 10 | 11 | module.exports = router; 12 | -------------------------------------------------------------------------------- /lib/shared/utils.js: -------------------------------------------------------------------------------- 1 | const csurf = require('csurf')({ cookie: true }); 2 | 3 | function returnView(tmplPath) { 4 | return (req, res) => { 5 | const tmpl = require(tmplPath); 6 | res.marko(tmpl); 7 | }; 8 | } 9 | 10 | function csrfIfSecure(req, res, next) { 11 | if (!req.shouldBeSecure) { 12 | return next(); 13 | } 14 | 15 | csurf(req, res, next); 16 | } 17 | 18 | module.exports = { returnView, csrfIfSecure }; 19 | -------------------------------------------------------------------------------- /lib/register/index.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { createUser } = require('../shared/db'); 3 | 4 | const parsePost = require('body-parser').urlencoded({ extended: false }); 5 | 6 | router.post('/', parsePost, async (req, res) => { 7 | const { username, password } = req.body; 8 | 9 | try { 10 | const data = createUser(username, password, 'user'); 11 | res.send(data); 12 | } catch (err) { 13 | console.error(err); 14 | res.status(500).send('Failed to register'); 15 | } 16 | }); 17 | 18 | module.exports = router; 19 | -------------------------------------------------------------------------------- /lib/csp-report/index.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { json: jsonParser } = require('body-parser'); 3 | 4 | function rewriteContentType(req, res, next) { 5 | if (req.get('Content-Type') === 'application/csp-report') { 6 | req.headers['content-type'] = 'application/json'; 7 | next(); 8 | } 9 | res.status(400).send(); 10 | } 11 | 12 | router.post('/', rewriteContentType, jsonParser(), (req, res) => { 13 | console.warn('CSP Violation'); 14 | console.log(req.body); 15 | res.send(); 16 | }); 17 | 18 | module.exports = router; 19 | -------------------------------------------------------------------------------- /lib/shared/https.js: -------------------------------------------------------------------------------- 1 | function isHttps(req) { 2 | return req && req.headers && req.headers['x-forwarded-proto'] === 'https'; 3 | } 4 | 5 | function getBaseHostUrl(req) { 6 | if (!req) { 7 | return null; 8 | } 9 | 10 | let protocol = isHttps(req) ? 'https' : 'http'; 11 | return `${protocol}://${req.get('host')}`; 12 | } 13 | 14 | function forceSsl(req, res, next) { 15 | if (isHttps(req)) { 16 | next(); 17 | } else { 18 | const url = `https://${req.get('host')}${req.url}`; 19 | res.redirect(307, url); 20 | } 21 | } 22 | 23 | module.exports = { 24 | forceSsl, 25 | isHttps, 26 | getBaseHostUrl 27 | }; -------------------------------------------------------------------------------- /lib/login/login.marko: -------------------------------------------------------------------------------- 1 | 2 | <@body> 3 |

Please log in to post something

4 |
5 |
6 | 7 | 8 |
9 |
10 | 11 | 12 |
13 | 14 |
15 | 16 | <@scripts> 17 | ', {headers: {'Content-Type':'text/html'}})) }else{e.fetch(e.request)}}//"))`; 85 | -------------------------------------------------------------------------------- /docs/security/content-security-policy.md: -------------------------------------------------------------------------------- 1 | # Content Security Policy (CSP) 2 | 3 | [Content Security Policy] is a security measurement that helps you to detect and mitigate certain set of attacks including [XSS](../vulnerabilities/cross-site-scripting.md). It allows you to specify rules for which content should be loaded by the browser. These rules can be set as an [HTTP header `Content-Security-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) or as a [`meta` tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta). 4 | 5 | The following is an example of such a header: 6 | 7 | ```http 8 | Content-Security-Policy: 9 | default-src 'self'; 10 | script-src 'nonce-NWo2+pmewRLPWqpsgv6J2w=='; 11 | style-src 'nonce-NWo2+pmewRLPWqpsgv6J2w=='; 12 | object-src 'none'; 13 | img-src 'self' api.adorable.io; 14 | font-src 'self' fonts.gstatic.com; 15 | block-all-mixed-content; 16 | report-uri /csp-report; 17 | ``` 18 | 19 | It enforces the following rules: 20 | 21 | - `script`s and `style`s should only be loaded if they contain a certain nonce that should differ on every request. A valid `script` would be: 22 | ```html 23 | 24 | ``` 25 | - `object`s are entirely banned 26 | - `img`s should only be loaded from the domain of the page or from `api.adorable.io` 27 | - `font`s should only be loaded from the domain of the page or from `fonts.gstatic.com` 28 | - All mixed content (i.e. `http` content on the `https` page) should be blocked 29 | - Any violations should be reported to `/csp-report` via a POST request. 30 | 31 | You can find more configuration options in the [MDN documenation about CSP]. Not every CSP configuration is equally secure. You should check out the paper by Google: [CSP Is Dead, Long Live CSP! On the Insecurity of 32 | Whitelists and the Future of Content Security Policy]. 33 | 34 | __CSP should never be your security strategy but rather a safety net.__ 35 | 36 | ### Resources/References 37 | - [MDN documenation about CSP] 38 | - [OWASP: Content Security Policy](https://www.owasp.org/index.php/Content_Security_Policy) 39 | - Paper: [CSP Is Dead, Long Live CSP! On the Insecurity of 40 | Whitelists and the Future of Content Security Policy] 41 | - [GitHub's CSP journey](https://githubengineering.com/githubs-csp-journey/) 42 | 43 | [Content Security Policy]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP 44 | [MDN documenation about CSP]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 45 | 46 | [CSP Is Dead, Long Live CSP! On the Insecurity of 47 | Whitelists and the Future of Content Security Policy]: https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/45542.pdf -------------------------------------------------------------------------------- /docs/vulnerabilities/cross-site-scripting.md: -------------------------------------------------------------------------------- 1 | # Cross-Site Scripting (XSS) 2 | 3 | ### What's the vulnerability? 4 | 5 | XSS is a common attack where the attacker is able inject and execute code in your page's context. This is particullary dangerous because it allows to execute logic in your own security and user context. It could be used to perform actions on behalf of the user, capture the user's input to steal information or many more things. 6 | 7 | ### How can the vulnerability be tested? 8 | 9 | Detecting XSS in general can be easy or hard. In general you should be sceptical of potential XSS attacks anywhere where the user is able to inject content on the page in any shape. This could be via a URL query parameter in the case of a search field, by posting content into a database or similar things. 10 | 11 | In our example the vulnerability lies in being able to post links to the [home page] that execute JavaScript on the page when clicked. You can try the following example. Post on the [home page] this content: 12 | 13 | ```markdown 14 | [Click me](javascript:this;alert('Oh hai!')) 15 | ``` 16 | 17 | Now when you click the link it should create an alert prompt with `Oh hai!`. 18 | 19 | ### What causes this vulnerability? 20 | 21 | In our case we are using an [old version of `marked` that has a vulnerability](https://snyk.io/vuln/npm:marked:20150520) even when being executed in `secure` mode. While injecting HTML directly is blocked and a classic `[Click me](javascript:alert(1))` wouldn't work either, using some encoding and a browser bug/feature we are able to circumvent the sanitaziation process. While this one has been fixed in a newer version of `marked`, [other vulnerabilites around `data:` links](https://snyk.io/vuln/npm:marked:20170112) still exist. 22 | 23 | Overall XSS vulnerabilities can be caused by different factors and most commonly by trusting a user input too much. This can also include dangerous CSS injections as shown in this [CSS-in-JS example](https://reactarmory.com/answers/how-can-i-use-css-in-js-securely). 24 | 25 | ### How can this be fixed? 26 | 27 | On the one hand you should make sure to not have old versions of your dependencies as they could have potential XSS attacks but generally speaking you should never trust your user's input. Even if you try to sanitize it, be aware of things like encodings to circumvent these sanitization processes. 28 | 29 | ### Resources/References 30 | - [Snyk Goof demo app with `marked` vulnerability](https://github.com/snyk/goof) 31 | - [OWASP: Cross-Site Scripting](https://www.owasp.org/index.php/Cross-site_Scripting_(XSS)) 32 | - [OWASP: XSS Prevention Cheat Sheet](https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet) 33 | - [OWASP: Testing for CSS injection](https://www.owasp.org/index.php/Testing_for_CSS_Injection_(OTG-CLIENT-005)) 34 | 35 | [home page]: https://onesie.life/home 36 | [login page]: https://onesie.life/login -------------------------------------------------------------------------------- /docs/vulnerabilities/clickjacking.md: -------------------------------------------------------------------------------- 1 | # Clickjacking 2 | 3 | ### What's the vulnerability? 4 | 5 | Clickjacking is an attack where the attacker is trying to make a user belief they are interacting with one site while in reality they are clicking on another. This attack is performed by laying the page they want the user to interact with on top of the page they want the user to believe to be clicking on. They then set the `opacity` to `0` to make the user only see the phishing UI. 6 | 7 | By strategically laying buttons above each other they can then trick the user into clicking on buttons they would otherwise probably wouldn't click. 8 | 9 | ![Clickjacking Example](https://www.owasp.org/images/8/84/Clickjacking_description.png) 10 | Source: https://www.owasp.org/index.php/Testing_for_Clickjacking_(OTG-CLIENT-009) 11 | 12 | ### How can the vulnerability be tested? 13 | 14 | If your page can be loaded in an `frame`/`iframe` it can potentially be used for clickjacking attacks. Try loading the [home page] in an iframe by adding this code to any page: 15 | 16 | ```html 17 | 18 | ``` 19 | 20 | ### What causes this vulnerability? 21 | 22 | By default any page can be iframed unless the browser is explicitly told not to. Since CSS allows you to layer things on top of each other and make things transparent, clickjacking can easily be achieved. 23 | 24 | ### How can this be fixed? 25 | 26 | You can tell the browser explictly not to load a page in a frame in two ways. One is by using a [CSP policy](../security/content-security-policy.md) the more browser compatible one, though, is using the `X-Frame-Options` HTTP header. 27 | 28 | Make sure your server sends the following HTTP header with every response: 29 | 30 | ```http 31 | X-Frame-Options: deny 32 | ``` 33 | 34 | This will by default block every case of framing your page. You can make this more permissive to allow framing on your own page or whitelist certain URLs. But unless this is really essential, you should just generally ban framing. 35 | 36 | You can test the header in action by trying to frame `https://onesie.life/secure/home` instead. 37 | 38 | Also be aware that framebusting scripts as sometimes suggested can be insufficient in most cases as explained in [this study](https://www.owasp.org/index.php/Testing_for_Clickjacking_(OTG-CLIENT-009)). 39 | 40 | ### Resources 41 | 42 | - [OWASP: Clickjacking Defense Cheat Sheet](https://www.owasp.org/index.php/Clickjacking_Defense_Cheat_Sheet) 43 | - [OWASP: Testing for clickjacking](https://www.owasp.org/index.php/Testing_for_Clickjacking_(OTG-CLIENT-009)) 44 | - [OWASP: Research on Framebusting](https://www.owasp.org/images/0/0e/OWASP_AppSec_Research_2010_Busting_Frame_Busting_by_Rydstedt.pdf) 45 | - [MDN page on `X-Frame-Options`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) 46 | 47 | 48 | [home page]: https://onesie.life/home 49 | [login page]: https://onesie.life/login -------------------------------------------------------------------------------- /lib/shared/db.js: -------------------------------------------------------------------------------- 1 | const twilio = require('twilio'); 2 | const argon2 = require('argon2'); 3 | const marked = require('marked'); 4 | const { escape } = require('querystring'); 5 | 6 | const { TWILIO_ACCOUNT_SID, TWILIO_API_KEY, TWILIO_API_SECRET } = process.env; 7 | const client = twilio(TWILIO_API_KEY, TWILIO_API_SECRET, { 8 | accountSid: TWILIO_ACCOUNT_SID 9 | }); 10 | const serviceSid = process.env.TWILIO_SYNC_SERVICE || 'default'; 11 | const sync = client.sync.services(serviceSid); 12 | 13 | const DB_NAMES = { 14 | POSTS: 'posts', 15 | USERS: 'users' 16 | }; 17 | 18 | const DEFAULT_POSTS = [ 19 | { 20 | name: 'Dom 🐼', 21 | message: `

Fluffiness on the train 22 |

23 | ` 24 | }, 25 | { 26 | name: 'Magic 🦄', 27 | message: `

We need more fluffiness in our lives!

` 28 | }, 29 | { 30 | name: 'Best 🐱', 31 | message: `

Fluffiness around the world 32 |

33 | ` 34 | }, 35 | { name: 'Dom 🐼', message: `

Everything is awesome! #onesiejs

` } 36 | ]; 37 | 38 | async function init() { 39 | // this just tries to get the respective element and else creates it 40 | try { 41 | await sync.syncLists(DB_NAMES.POSTS).fetch(); 42 | } catch (err) { 43 | await sync.syncLists.create({ uniqueName: DB_NAMES.POSTS }); 44 | } 45 | 46 | try { 47 | await sync.syncMaps(DB_NAMES.USERS).fetch(); 48 | } catch (err) { 49 | await sync.syncMaps.create({ uniqueName: DB_NAMES.USERS }); 50 | } 51 | 52 | return true; 53 | } 54 | 55 | async function reset() { 56 | const syncList = sync.syncLists(DB_NAMES.POSTS); 57 | await syncList.remove(); 58 | await sync.syncLists.create({ uniqueName: DB_NAMES.POSTS }); 59 | for (let data of DEFAULT_POSTS) { 60 | await syncList.syncListItems.create({ data }); 61 | } 62 | return true; 63 | } 64 | 65 | async function getPosts() { 66 | return (await sync 67 | .syncLists(DB_NAMES.POSTS) 68 | .syncListItems.list({ order: 'desc' })).map(x => x.data); 69 | } 70 | 71 | async function createPost(name, message) { 72 | message = marked(message, { sanitize: true }); 73 | 74 | const data = { message, name }; 75 | await sync.syncLists(DB_NAMES.POSTS).syncListItems.create({ data }); 76 | return data; 77 | } 78 | 79 | async function createUser(username, password, role) { 80 | // this will automatically salt the password and store the password as part of the hash 81 | // as recommended in https://www.owasp.org/index.php/Password_Storage_Cheat_Sheet 82 | const hash = await argon2.hash(password); 83 | const data = { username, hash, role }; 84 | 85 | await sync 86 | .syncMaps(DB_NAMES.USERS) 87 | .syncMapItems.create({ key: username, data }); 88 | 89 | return { username, role }; 90 | } 91 | 92 | async function authenticateUser(username, password) { 93 | let item; 94 | try { 95 | item = await sync 96 | .syncMaps(DB_NAMES.USERS) 97 | .syncMapItems(escape(username)) 98 | .fetch(); 99 | } catch (err) { 100 | console.error(err); 101 | return null; 102 | } 103 | 104 | const userData = item.data; 105 | const passwordIsCorrect = await argon2.verify(userData.hash, password); 106 | 107 | if (!passwordIsCorrect) { 108 | return null; 109 | } 110 | return { role: userData.role, username }; 111 | } 112 | 113 | module.exports = { 114 | init, 115 | reset, 116 | getPosts, 117 | createPost, 118 | createUser, 119 | authenticateUser 120 | }; 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

💖

2 |

onesie.life

3 |

An intentionally insecure web application to highlight different web security concepts

4 | 5 | --- 6 | 7 | This is an example application used by [Dominik Kundel](https://github.com/dkundel) in his [Introduction to Web Security talk](https://github.com/dkundel/intro-web-security). It has intenionally a [set of vulnerabilities](#vulnerabilities) to highlight different attack vectors and as well as ways to fix them. 8 | 9 | If you find any additional attack vectors, feel free to create an issue for it or alternatively create a pull request for this README to add it to the [list of vulnerabilities](#vulnerabilities). 10 | 11 | ## Vulnerabilities 12 | There is a variety of vulnerabilites present in this application. Check out the respective docs to learn more about them. 13 | 14 | - [Clickjacking](docs/vulnerabilities/clickjacking.md) 15 | - [Cross Site Request Forgery (CSRF)](docs/vulnerabilities/cross-site-request-forgery.md) 16 | - [Cross Site Scripting (XSS)](docs/vulnerabilities/cross-site-scripting.md) 17 | - [JSON Web Token (JWT)](docs/vulnerabilities/json-web-token.md) 18 | - [Poor JSONP implementations](docs/vulnerabilities/jsonp.md) 19 | - [Parent Window Redirection](docs/vulnerabilities/window-redirection.md) 20 | 21 | ## Security Measurements 22 | 23 | - [Content Security Policy](docs/security/content-security-policy.md) 24 | 25 | ## Resources 26 | 27 | - [Open Web Application Security Project (OWASP)](https://www.owasp.org/index.php/Main_Page). Extensive Wiki around all web security related topics 28 | - [OWASP Common Attacks List](https://www.owasp.org/index.php/Category:Attack) 29 | - [Slides of my intro to web security talk](https://github.com/dkundel/intro-web-security) 30 | - [Google Web Fundamentals Security](https://developers.google.com/web/fundamentals/security/) 31 | - [Gruyere Codelab](https://google-gruyere.appspot.com/). A Codelab by Google teaching you different things around security 32 | - [SecurityHeaders.io](https://securityheaders.io). Analyzes the HTTP headers of your application for security aspects 33 | - [`goof`](https://github.com/snyk/goof). A vulnerable demo app by Synk.io 34 | - [`helmet`](https://helmetjs.github.io/). A Node.js module to set security related headers for your [`express`](https://npm.im/express) server 35 | - [Snyk.io](https://snyk.io). A tool to detect vulnerabilities in your projects by scanning your dependencies 36 | - [Greenkeeper.io](https://greenkeeper.io/). A tool to keep your dependencies up to date 37 | 38 | ## Setup 39 | 40 | This application is built with Node.js and uses Twilio Sync as a database at the moment. 41 | 42 | ### Prerequisites 43 | - [Node.js](https://nodejs.org) & [npm](https://npmjs.com) 44 | - A Twilio account - [Sign up here](https://www.twilio.com/try-twilio) 45 | 46 | Make sure you have the following values [stored in your environment variables](https://www.twilio.com/blog/2017/01/how-to-set-environment-variables.html): 47 | 48 | ```bash 49 | # Your Twilio Account SID 50 | TWILIO_ACCOUNT_SID= 51 | # A Twilio API Key 52 | TWILIO_API_KEY= 53 | # A Twilio API Secret 54 | TWILIO_API_SECRET= 55 | # The SID of your Twilio Sync Service (can be 'default') 56 | TWILIO_SYNC_SERVICE=default 57 | ``` 58 | 59 | ### Setup 60 | 61 | ```bash 62 | git clone git@github.com:dkundel/onesie-life.git 63 | cd onesie-life 64 | npm install 65 | ``` 66 | 67 | ### Start Server 68 | 69 | ```bash 70 | npm start 71 | ``` 72 | 73 | ### Open Page http://localhost:3000 74 | 75 | ## License 76 | 77 | MIT 78 | 79 | ## Contributors 80 | 81 | - [Dominik Kundel](https://github.com/dkundel) -------------------------------------------------------------------------------- /docs/vulnerabilities/jsonp.md: -------------------------------------------------------------------------------- 1 | # Poor JSON with Padding (JSONP) Implementation 2 | 3 | ### What's the vulnerability? 4 | 5 | [JSONP] is commonly used to circumvent things like [Same-Origin Policy](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy) if you are trying to fetch data asynchronously from a different domain. For this the developer will expose a function in the global namespace that should be called when the data is being received. To then request the data, they inject a new `script` tag into the page to a [JSONP] enabled endpoint with a `callback` parameter that specifies the function name that should be called. 6 | 7 | The server then wraps the data that it's returning into the respective function call: 8 | 9 | ```js 10 | // Example /jsonp?callback=myDataCallback 11 | // server responds with: 12 | myDataCallback({ data: [/*...*/] }) 13 | ``` 14 | 15 | If a [JSONP] endpoint is poorly implemented you could use this to generate your own JavaScript though: 16 | 17 | ```js 18 | // Example /jsonp?callback=alert(1);// 19 | alert(1);//({ data: [/*...*/]}) 20 | ``` 21 | 22 | As you can see we are not even interested in the actual data in this case. But we can use this in combination with an existing XSS vulnerability for example to install [dangerous service worker implemenations](https://c0nradsc0rner.wordpress.com/2016/06/17/xss-persistence-using-jsonp-and-serviceworkers/). 23 | 24 | ### How can the vulnerability be tested? 25 | 26 | You can try this by combining it with our [existing XSS vulnerability](cross-site-scripting.md) in the application by visiting the [home page] and posting the following content: 27 | 28 | ```markdown 29 | [You won't believe this!!!!!!](javascript:this;navigator.serviceWorker.register("/post?callback=onfetch=function(e){if(!(e.request.url.indexOf(':4000')>0)){e.respondWith(new Response('

Hacked

', {headers: {'Content-Type':'text/html'}})) }else{e.fetch(e.request)}}//")) 30 | ``` 31 | 32 | If you then click the link, it will install a service worker. From that point on any resource the browser is trying to fetch will be overriden with the following HTML: 33 | 34 | ```html 35 |

Hacked

36 | 37 | ``` 38 | 39 | While in this case we are overriding the whole HTML an attacker could also fetch the original one and only append/inject their own script. 40 | 41 | To uninstall the service worker in Chrome open the developer tools, click on the Applications tab and unregister the service worker you see. 42 | 43 | ### What causes this vulnerability? 44 | 45 | By not properly checking the value of the `callback` query parameter we basically allow to pass in aribitary JavaScript since the attacker could just end their input with `//` to turn the remaining part into a comment. 46 | 47 | Since [Service Workers] have to be from the same domain as the page itself, it's the perfect way for an attacker to inject their own service worker. 48 | 49 | ### How can this be fixed? 50 | 51 | You should either only whitelist a set of valid `callback` values or verify that the callback parameter only contains word characters but nothing else. 52 | 53 | ### Resources/References 54 | - [XSS persistence using JSONP and serviceWorkers](https://c0nradsc0rner.wordpress.com/2016/06/17/xss-persistence-using-jsonp-and-serviceworkers/) 55 | - [OWASP: From JSONP to XSS Persistence](https://www.owasp.org/images/3/35/2017-04-20-JSONPXSS.pdf) 56 | 57 | [JSONP]: https://en.wikipedia.org/wiki/JSONP 58 | [home page]: https://onesie.life/home 59 | [login page]: https://onesie.life/login 60 | [Service Workers]: https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API -------------------------------------------------------------------------------- /public/images/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/vulnerabilities/json-web-token.md: -------------------------------------------------------------------------------- 1 | # Insecure JSON Web Token (JWT) 2 | 3 | ### What's the vulnerability? 4 | 5 | The application uses [JSON Web Tokens] (JWT) to handle the sessions of users. This is a common way to do it as it doesn't require any session storage on the application side. The token is transmitted to the user via a cookie with the name `authToken`. The current implementation has a significant vulnerability that allows an attacker to impersonate another user and gain admin access rights. 6 | 7 | ### How can the vulnerability be tested? 8 | 9 | Open your browser and navigate to the [login page]. Open the developer tools and type in the following code into the JavaScript console: 10 | 11 | ```js 12 | Cookies.set('authToken', 'INSERT JWT TOKEN'); 13 | ``` 14 | 15 | Now navigate to the [home page] and you should be logged in as indicated at the top. 16 | 17 | Inspect the token we set on jwt.io and modify the payload attributes `role` to `admin` or `sub` to any username you would like. 18 | 19 | ### What causes this vulnerability? 20 | 21 | _a) Vulnerable JWT implementation_ 22 | 23 | The [JWT spec] suggests a variety of algorithms that can be used for the signature. One valid algorithm for the signature is called `none` intended for tokens where the integraty has been verified in a different way. The algorithm is specified in the header of the token. Note that the header is not encrypted and can therefore be altered. If a library is strictly implemented according to the spec and determines the algorithm based on the `alg` field in the header it is possible to circumvent the verification step by creating a token with the following header: 24 | 25 | ```json 26 | { 27 | "alg": "none" 28 | } 29 | ``` 30 | 31 | We can now turn this into a `base64` encoded string, replace the original header and remove the signature and have a valid token. 32 | 33 | In our case we are using [`jsonwebtoken`](npm.im/jsonwebtoken) implementation at version `0.4.0` which is implemented according to spec but does contain exactly this vulnerability. 34 | 35 | _b) Insecure use of cookies_ 36 | 37 | The application is using cookies that should be `HttpOnly` meaning they should not be accessible in JavaScript as they are not needed there. Additionally they should be signed with a secret key to make sure they can't be tempered with. 38 | 39 | ### How can this be fixed? 40 | 41 | _a) Vulnerable JWT implementation_ 42 | 43 | 1. Use a secure implementation of the [JWT spec]. They should allow to specify a set of valid algorithms to disallow `none` or algorithms you are not expecting. The latest version of [jsonwebtoken](https://npm.im/jsonwebtoken) fullfils this criteria 44 | 2. Always keep up to date with the version to avoid exposing known security vulnerabilities 45 | 3. Consider replacing the JWT header on the server side with the one you expect. 46 | 47 | _b) Insecure use of cookies_ 48 | 49 | 1. Use `HttpOnly` cookies 50 | 2. Use signed and secure cookies 51 | 52 | ```js 53 | req.cookie('authToken', token, { 54 | httpOnly: true, 55 | secure: true, 56 | signed: true 57 | }); 58 | ``` 59 | 60 | ### Resources/References 61 | - [JWT spec] 62 | - [Blog post "Stop using JWT for sessions"](http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/) 63 | - [Auth0: Critical Vulnerability in JWT libraries](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/) 64 | - [Troy Hunt: C is for Cookie, H is for Hacker](https://www.troyhunt.com/c-is-for-cookie-h-is-for-hacker/) 65 | - [OWASP: JWT Cheat Sheet for Java](https://www.owasp.org/index.php/JSON_Web_Token_(JWT)_Cheat_Sheet_for_Java) 66 | - [OWASP: `HttpOnly` cookies](https://www.owasp.org/index.php/HttpOnly) 67 | - [MDN: `Set-Cookie` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie) 68 | - [CodingHorror: Protecting your cookies `HttpOnly`](https://blog.codinghorror.com/protecting-your-cookies-httponly/) 69 | 70 | [JWT spec]: https://tools.ietf.org/html/rfc7519 71 | [JSON Web Tokens]: https://jwt.io 72 | [home page]: https://onesie.life/home 73 | [login page]: https://onesie.life/login 74 | --------------------------------------------------------------------------------