From afc0ea0a58c0e0c36ac187a0a132fc905c47a728 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Sun, 22 Feb 2015 18:15:22 -0600 Subject: [PATCH] Add csrf prevention --- lib/web/account.js | 9 +++++++ lib/web/auth.js | 8 ++++++ lib/web/csrf.js | 40 ++++++++++++++++++++++++++++ lib/web/jade.js | 3 ++- lib/web/webserver.js | 6 +++++ package.json | 4 ++- templates/account-channels.jade | 2 ++ templates/account-edit.jade | 2 ++ templates/account-passwordreset.jade | 1 + templates/account-profile.jade | 1 + templates/csrferror.jade | 30 +++++++++++++++++++++ templates/login.jade | 1 + templates/nav.jade | 7 ++--- templates/register.jade | 1 + 14 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 lib/web/csrf.js create mode 100644 templates/csrferror.jade diff --git a/lib/web/account.js b/lib/web/account.js index 20a9dc27..19d61fff 100644 --- a/lib/web/account.js +++ b/lib/web/account.js @@ -12,6 +12,7 @@ var $util = require("../utilities"); var Config = require("../config"); var Server = require("../server"); var session = require("../session"); +var csrf = require("./csrf"); /** * Handles a GET request for /account/edit @@ -28,6 +29,8 @@ function handleAccountEditPage(req, res) { * Handles a POST request to edit a user"s account */ function handleAccountEdit(req, res) { + csrf.verify(req); + var action = req.body.action; switch(action) { case "change_password": @@ -204,6 +207,8 @@ function handleAccountChannelPage(req, res) { * Handles a POST request to modify a user"s channels */ function handleAccountChannel(req, res) { + csrf.verify(req); + var action = req.body.action; switch(action) { case "new_channel": @@ -394,6 +399,8 @@ function handleAccountProfilePage(req, res) { * Handles a POST request to edit a profile */ function handleAccountProfile(req, res) { + csrf.verify(req); + if (!req.user) { return sendJade(res, "account-profile", { profileImage: "", @@ -442,6 +449,8 @@ function handlePasswordResetPage(req, res) { * Handles a POST request to reset a user's password */ function handlePasswordReset(req, res) { + csrf.verify(req); + var name = req.body.name, email = req.body.email; diff --git a/lib/web/auth.js b/lib/web/auth.js index 5d851d39..09c7384e 100644 --- a/lib/web/auth.js +++ b/lib/web/auth.js @@ -15,11 +15,14 @@ var db = require("../database"); var Config = require("../config"); var url = require("url"); var session = require("../session"); +var csrf = require("./csrf"); /** * Processes a login request. Sets a cookie upon successful authentication */ function handleLogin(req, res) { + csrf.verify(req); + var name = req.body.name; var password = req.body.password; var rememberMe = req.body.remember; @@ -119,7 +122,10 @@ function handleLoginPage(req, res) { * Handles a request for /logout. Clears auth cookie */ function handleLogout(req, res) { + csrf.verify(req); + res.clearCookie("auth"); + req.user = res.user = null; // Try to find an appropriate redirect var dest = req.query.dest || req.header("referer"); dest = dest && dest.match(/login|logout|account/) ? null : dest; @@ -159,6 +165,8 @@ function handleRegisterPage(req, res) { * Processes a registration request. */ function handleRegister(req, res) { + csrf.verify(req); + var name = req.body.name; var password = req.body.password; var email = req.body.email; diff --git a/lib/web/csrf.js b/lib/web/csrf.js new file mode 100644 index 00000000..77d62144 --- /dev/null +++ b/lib/web/csrf.js @@ -0,0 +1,40 @@ +/* + * Adapted from https://github.com/expressjs/csurf + */ + +var csrf = require("csrf"); +var createError = require("http-errors"); + +var tokens = csrf(); + +exports.init = function csrfInit(req, res, next) { + var secret = req.signedCookies._csrf; + if (!secret) { + secret = tokens.secretSync(); + res.cookie("_csrf", secret, { signed: true, httpOnly: true }); + } + + var token; + + req.csrfToken = function csrfToken() { + if (token) { + return token; + } + + token = tokens.create(secret); + return token; + }; + + next(); +}; + +exports.verify = function csrfVerify(req) { + var secret = req.signedCookies._csrf; + var token = req.body._csrf || req.query._csrf; + + if (!tokens.verify(secret, token)) { + throw createError(403, 'invalid csrf token', { + code: 'EBADCSRFTOKEN' + }); + } +}; diff --git a/lib/web/jade.js b/lib/web/jade.js index 7b34ac8b..f7a96b8a 100644 --- a/lib/web/jade.js +++ b/lib/web/jade.js @@ -14,7 +14,8 @@ function merge(locals, res) { siteDescription: Config.get("html-template.description"), siteAuthor: "Calvin 'calzoneman' 'cyzon' Montgomery", loginDomain: Config.get("https.enabled") ? Config.get("https.full-address") - : Config.get("http.full-address") + : Config.get("http.full-address"), + csrfToken: res.req.csrfToken() }; if (typeof locals !== "object") { return _locals; diff --git a/lib/web/webserver.js b/lib/web/webserver.js index 2b3c861a..68eabe68 100644 --- a/lib/web/webserver.js +++ b/lib/web/webserver.js @@ -14,6 +14,7 @@ var cookieParser = require("cookie-parser"); var static = require("serve-static"); var morgan = require("morgan"); var session = require("../session"); +var csrf = require("./csrf"); const LOG_FORMAT = ':real-address - :remote-user [:date] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"'; morgan.token('real-address', function (req) { return req._ip; }); @@ -190,6 +191,7 @@ module.exports = { Logger.errlog.log("YOU SHOULD CHANGE THE VALUE OF cookie-secret IN config.yaml"); } app.use(cookieParser(Config.get("http.cookie-secret"))); + app.use(csrf.init); app.use(morgan(LOG_FORMAT, { stream: require("fs").createWriteStream(path.join(__dirname, "..", "..", "http.log"), { @@ -253,6 +255,10 @@ module.exports = { return res.status(413).end(); } else if (err.message && err.message.match(/bad request/i)) { return res.status(400).end("Bad Request"); + } else if (err.message && err.message.match(/invalid csrf token/i)) { + res.status(403); + sendJade(res, 'csrferror', { path: req.path }); + return; } Logger.errlog.log(err.stack); res.status(500).end(); diff --git a/package.json b/package.json index daa9c601..7b644478 100644 --- a/package.json +++ b/package.json @@ -9,13 +9,15 @@ "dependencies": { "bcrypt": "^0.8.1", "body-parser": "^1.10.2", - "cheerio" : "^0.18.0", + "cheerio": "^0.18.0", "compression": "^1.3.0", "cookie-parser": "^1.3.3", + "csrf": "^2.0.6", "cytubefilters": "git://github.com/calzoneman/cytubefilters#33b7693c", "express": "^4.11.1", "express-minify": "^0.1.3", "graceful-fs": "^3.0.5", + "http-errors": "^1.3.1", "jade": "^1.9.1", "json-typecheck": "^0.1.3", "morgan": "^1.5.1", diff --git a/templates/account-channels.jade b/templates/account-channels.jade index a173f826..dad5def5 100644 --- a/templates/account-channels.jade +++ b/templates/account-channels.jade @@ -39,6 +39,7 @@ html(lang="en") tr th form.form-inline.pull-right(action="/account/channels", method="post", onsubmit="return confirm('Are you sure you want to delete #{c.name}? This cannot be undone');") + input(type="hidden", name="_csrf", value=csrfToken) input(type="hidden", name="action", value="delete_channel") input(type="hidden", name="name", value="#{c.name}") button.btn.btn-xs.btn-danger(type="submit") Delete @@ -51,6 +52,7 @@ html(lang="en") strong Channel Registration Failed p= newChannelError form(action="/account/channels", method="post") + input(type="hidden", name="_csrf", value=csrfToken) input(type="hidden", name="action", value="new_channel") .form-group label.control-label(for="channelname") Channel Name diff --git a/templates/account-edit.jade b/templates/account-edit.jade index 549d17d1..55ce9f4c 100644 --- a/templates/account-edit.jade +++ b/templates/account-edit.jade @@ -29,6 +29,7 @@ html(lang="en") p= errorMessage h3 Change Password form(action="/account/edit", method="post", onsubmit="return validatePasswordChange()") + input(type="hidden", name="_csrf", value=csrfToken) input(type="hidden", name="action", value="change_password") .form-group label.control-label(for="username") Username @@ -46,6 +47,7 @@ html(lang="en") hr h3 Change Email form(action="/account/edit", method="post", onsubmit="return submitEmail()") + input(type="hidden", name="_csrf", value=csrfToken) input(type="hidden", name="action", value="change_email") .form-group label.control-label(for="username2") Username diff --git a/templates/account-passwordreset.jade b/templates/account-passwordreset.jade index def05b12..957dc794 100644 --- a/templates/account-passwordreset.jade +++ b/templates/account-passwordreset.jade @@ -25,6 +25,7 @@ html(lang="en") strong Error p= resetErr form(action="/account/passwordreset", method="post", role="form") + input(type="hidden", name="_csrf", value=csrfToken) .form-group label.control-label(for="username") Username input#username.form-control(type="text", name="name") diff --git a/templates/account-profile.jade b/templates/account-profile.jade index c0dbed7f..30e6a551 100644 --- a/templates/account-profile.jade +++ b/templates/account-profile.jade @@ -32,6 +32,7 @@ html(lang="en") p= profileText h3 Edit Profile form(action="/account/profile", method="post", role="form") + input(type="hidden", name="_csrf", value=csrfToken) .form-group label.control-label(for="profileimage") Image input#profileimage.form-control(type="text", name="image") diff --git a/templates/csrferror.jade b/templates/csrferror.jade new file mode 100644 index 00000000..b67bd428 --- /dev/null +++ b/templates/csrferror.jade @@ -0,0 +1,30 @@ +doctype html +html(lang="en") + head + include head + mixin head() + body + #wrap + nav.navbar.navbar-inverse.navbar-fixed-top(role="navigation") + include nav + mixin navheader() + #nav-collapsible.collapse.navbar-collapse + ul.nav.navbar-nav + mixin navdefaultlinks(path) + mixin navloginlogout(path) + + section#mainpage.container + .col-md-12 + .alert.alert-danger + h1 Invalid Session + p Your browser attempted to submit form data to #{path} with an invalid authentication token. This may be because: + ul + li Your session has expired + li Your request was missing the authentication token + li A malicious user has attempted to tamper with your session + li Your browser does not support cookies, or they are not enabled + | If the problem persists, please contact an administrator. + a(href=path) Return to previous page + + include footer + mixin footer() diff --git a/templates/login.jade b/templates/login.jade index 3ebf3ca5..c077243e 100644 --- a/templates/login.jade +++ b/templates/login.jade @@ -26,6 +26,7 @@ html(lang="en") p= loginError h2 Login form(role="form", action="/login", method="post") + input(type="hidden", name="_csrf", value=csrfToken) if redirect input(type="hidden", name="dest", value=redirect) .form-group diff --git a/templates/nav.jade b/templates/nav.jade index 8791f091..7bad7c47 100644 --- a/templates/nav.jade +++ b/templates/nav.jade @@ -47,7 +47,8 @@ mixin navloginform(redirect) - loginDomain = "" .visible-lg form#loginform.navbar-form.navbar-right(action="#{loginDomain}/login", method="post") - input(type="hidden", name="dest", value=redirect) + input(type="hidden", name="_csrf", value=csrfToken) + input(type="hidden", name="dest", value=encodeURIComponent(redirect)) .form-group input#username.form-control(type="text", name="name", placeholder="Username") .form-group @@ -60,7 +61,7 @@ mixin navloginform(redirect) button#login.btn.btn-default(type="submit") Login .visible-md p#loginform.navbar-text.pull-right - a#login.navbar-link(href="#{loginDomain}/login?dest=#{redirect}") Log in + a#login.navbar-link(href="#{loginDomain}/login?dest=#{encodeURIComponent(redirect)}") Log in span  ·  a#register.navbar-link(href="/register") Register @@ -69,4 +70,4 @@ mixin navlogoutform(redirect) p#logoutform.navbar-text.pull-right span#welcome Welcome, #{loginName} span  ·  - a#logout.navbar-link(href="/logout?dest=#{redirect}") Logout + a#logout.navbar-link(href="/logout?dest=#{encodeURIComponent(redirect)}&_csrf=#{csrfToken}") Logout diff --git a/templates/register.jade b/templates/register.jade index c14676af..3942ecca 100644 --- a/templates/register.jade +++ b/templates/register.jade @@ -29,6 +29,7 @@ html(lang="en") p= registerError h2 Register form(role="form", action="/register", method="post", onsubmit="return verify()") + input(type="hidden", name="_csrf", value=csrfToken) .form-group label.control-label(for="username") Username input#username.form-control(type="text", name="name")