Add csrf prevention

This commit is contained in:
calzoneman 2015-02-22 18:15:22 -06:00
parent 420e77963b
commit afc0ea0a58
14 changed files with 110 additions and 5 deletions

View File

@ -12,6 +12,7 @@ var $util = require("../utilities");
var Config = require("../config"); var Config = require("../config");
var Server = require("../server"); var Server = require("../server");
var session = require("../session"); var session = require("../session");
var csrf = require("./csrf");
/** /**
* Handles a GET request for /account/edit * 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 * Handles a POST request to edit a user"s account
*/ */
function handleAccountEdit(req, res) { function handleAccountEdit(req, res) {
csrf.verify(req);
var action = req.body.action; var action = req.body.action;
switch(action) { switch(action) {
case "change_password": case "change_password":
@ -204,6 +207,8 @@ function handleAccountChannelPage(req, res) {
* Handles a POST request to modify a user"s channels * Handles a POST request to modify a user"s channels
*/ */
function handleAccountChannel(req, res) { function handleAccountChannel(req, res) {
csrf.verify(req);
var action = req.body.action; var action = req.body.action;
switch(action) { switch(action) {
case "new_channel": case "new_channel":
@ -394,6 +399,8 @@ function handleAccountProfilePage(req, res) {
* Handles a POST request to edit a profile * Handles a POST request to edit a profile
*/ */
function handleAccountProfile(req, res) { function handleAccountProfile(req, res) {
csrf.verify(req);
if (!req.user) { if (!req.user) {
return sendJade(res, "account-profile", { return sendJade(res, "account-profile", {
profileImage: "", profileImage: "",
@ -442,6 +449,8 @@ function handlePasswordResetPage(req, res) {
* Handles a POST request to reset a user's password * Handles a POST request to reset a user's password
*/ */
function handlePasswordReset(req, res) { function handlePasswordReset(req, res) {
csrf.verify(req);
var name = req.body.name, var name = req.body.name,
email = req.body.email; email = req.body.email;

View File

@ -15,11 +15,14 @@ var db = require("../database");
var Config = require("../config"); var Config = require("../config");
var url = require("url"); var url = require("url");
var session = require("../session"); var session = require("../session");
var csrf = require("./csrf");
/** /**
* Processes a login request. Sets a cookie upon successful authentication * Processes a login request. Sets a cookie upon successful authentication
*/ */
function handleLogin(req, res) { function handleLogin(req, res) {
csrf.verify(req);
var name = req.body.name; var name = req.body.name;
var password = req.body.password; var password = req.body.password;
var rememberMe = req.body.remember; var rememberMe = req.body.remember;
@ -119,7 +122,10 @@ function handleLoginPage(req, res) {
* Handles a request for /logout. Clears auth cookie * Handles a request for /logout. Clears auth cookie
*/ */
function handleLogout(req, res) { function handleLogout(req, res) {
csrf.verify(req);
res.clearCookie("auth"); res.clearCookie("auth");
req.user = res.user = null;
// Try to find an appropriate redirect // Try to find an appropriate redirect
var dest = req.query.dest || req.header("referer"); var dest = req.query.dest || req.header("referer");
dest = dest && dest.match(/login|logout|account/) ? null : dest; dest = dest && dest.match(/login|logout|account/) ? null : dest;
@ -159,6 +165,8 @@ function handleRegisterPage(req, res) {
* Processes a registration request. * Processes a registration request.
*/ */
function handleRegister(req, res) { function handleRegister(req, res) {
csrf.verify(req);
var name = req.body.name; var name = req.body.name;
var password = req.body.password; var password = req.body.password;
var email = req.body.email; var email = req.body.email;

40
lib/web/csrf.js Normal file
View File

@ -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'
});
}
};

View File

@ -14,7 +14,8 @@ function merge(locals, res) {
siteDescription: Config.get("html-template.description"), siteDescription: Config.get("html-template.description"),
siteAuthor: "Calvin 'calzoneman' 'cyzon' Montgomery", siteAuthor: "Calvin 'calzoneman' 'cyzon' Montgomery",
loginDomain: Config.get("https.enabled") ? Config.get("https.full-address") 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") { if (typeof locals !== "object") {
return _locals; return _locals;

View File

@ -14,6 +14,7 @@ var cookieParser = require("cookie-parser");
var static = require("serve-static"); var static = require("serve-static");
var morgan = require("morgan"); var morgan = require("morgan");
var session = require("../session"); 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"'; 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; }); 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"); Logger.errlog.log("YOU SHOULD CHANGE THE VALUE OF cookie-secret IN config.yaml");
} }
app.use(cookieParser(Config.get("http.cookie-secret"))); app.use(cookieParser(Config.get("http.cookie-secret")));
app.use(csrf.init);
app.use(morgan(LOG_FORMAT, { app.use(morgan(LOG_FORMAT, {
stream: require("fs").createWriteStream(path.join(__dirname, "..", "..", stream: require("fs").createWriteStream(path.join(__dirname, "..", "..",
"http.log"), { "http.log"), {
@ -253,6 +255,10 @@ module.exports = {
return res.status(413).end(); return res.status(413).end();
} else if (err.message && err.message.match(/bad request/i)) { } else if (err.message && err.message.match(/bad request/i)) {
return res.status(400).end("Bad Request"); 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); Logger.errlog.log(err.stack);
res.status(500).end(); res.status(500).end();

View File

@ -9,13 +9,15 @@
"dependencies": { "dependencies": {
"bcrypt": "^0.8.1", "bcrypt": "^0.8.1",
"body-parser": "^1.10.2", "body-parser": "^1.10.2",
"cheerio" : "^0.18.0", "cheerio": "^0.18.0",
"compression": "^1.3.0", "compression": "^1.3.0",
"cookie-parser": "^1.3.3", "cookie-parser": "^1.3.3",
"csrf": "^2.0.6",
"cytubefilters": "git://github.com/calzoneman/cytubefilters#33b7693c", "cytubefilters": "git://github.com/calzoneman/cytubefilters#33b7693c",
"express": "^4.11.1", "express": "^4.11.1",
"express-minify": "^0.1.3", "express-minify": "^0.1.3",
"graceful-fs": "^3.0.5", "graceful-fs": "^3.0.5",
"http-errors": "^1.3.1",
"jade": "^1.9.1", "jade": "^1.9.1",
"json-typecheck": "^0.1.3", "json-typecheck": "^0.1.3",
"morgan": "^1.5.1", "morgan": "^1.5.1",

View File

@ -39,6 +39,7 @@ html(lang="en")
tr tr
th 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');") 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="action", value="delete_channel")
input(type="hidden", name="name", value="#{c.name}") input(type="hidden", name="name", value="#{c.name}")
button.btn.btn-xs.btn-danger(type="submit") Delete button.btn.btn-xs.btn-danger(type="submit") Delete
@ -51,6 +52,7 @@ html(lang="en")
strong Channel Registration Failed strong Channel Registration Failed
p= newChannelError p= newChannelError
form(action="/account/channels", method="post") form(action="/account/channels", method="post")
input(type="hidden", name="_csrf", value=csrfToken)
input(type="hidden", name="action", value="new_channel") input(type="hidden", name="action", value="new_channel")
.form-group .form-group
label.control-label(for="channelname") Channel Name label.control-label(for="channelname") Channel Name

View File

@ -29,6 +29,7 @@ html(lang="en")
p= errorMessage p= errorMessage
h3 Change Password h3 Change Password
form(action="/account/edit", method="post", onsubmit="return validatePasswordChange()") form(action="/account/edit", method="post", onsubmit="return validatePasswordChange()")
input(type="hidden", name="_csrf", value=csrfToken)
input(type="hidden", name="action", value="change_password") input(type="hidden", name="action", value="change_password")
.form-group .form-group
label.control-label(for="username") Username label.control-label(for="username") Username
@ -46,6 +47,7 @@ html(lang="en")
hr hr
h3 Change Email h3 Change Email
form(action="/account/edit", method="post", onsubmit="return submitEmail()") form(action="/account/edit", method="post", onsubmit="return submitEmail()")
input(type="hidden", name="_csrf", value=csrfToken)
input(type="hidden", name="action", value="change_email") input(type="hidden", name="action", value="change_email")
.form-group .form-group
label.control-label(for="username2") Username label.control-label(for="username2") Username

View File

@ -25,6 +25,7 @@ html(lang="en")
strong Error strong Error
p= resetErr p= resetErr
form(action="/account/passwordreset", method="post", role="form") form(action="/account/passwordreset", method="post", role="form")
input(type="hidden", name="_csrf", value=csrfToken)
.form-group .form-group
label.control-label(for="username") Username label.control-label(for="username") Username
input#username.form-control(type="text", name="name") input#username.form-control(type="text", name="name")

View File

@ -32,6 +32,7 @@ html(lang="en")
p= profileText p= profileText
h3 Edit Profile h3 Edit Profile
form(action="/account/profile", method="post", role="form") form(action="/account/profile", method="post", role="form")
input(type="hidden", name="_csrf", value=csrfToken)
.form-group .form-group
label.control-label(for="profileimage") Image label.control-label(for="profileimage") Image
input#profileimage.form-control(type="text", name="image") input#profileimage.form-control(type="text", name="image")

30
templates/csrferror.jade Normal file
View File

@ -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 <code>#{path}</code> 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()

View File

@ -26,6 +26,7 @@ html(lang="en")
p= loginError p= loginError
h2 Login h2 Login
form(role="form", action="/login", method="post") form(role="form", action="/login", method="post")
input(type="hidden", name="_csrf", value=csrfToken)
if redirect if redirect
input(type="hidden", name="dest", value=redirect) input(type="hidden", name="dest", value=redirect)
.form-group .form-group

View File

@ -47,7 +47,8 @@ mixin navloginform(redirect)
- loginDomain = "" - loginDomain = ""
.visible-lg .visible-lg
form#loginform.navbar-form.navbar-right(action="#{loginDomain}/login", method="post") 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 .form-group
input#username.form-control(type="text", name="name", placeholder="Username") input#username.form-control(type="text", name="name", placeholder="Username")
.form-group .form-group
@ -60,7 +61,7 @@ mixin navloginform(redirect)
button#login.btn.btn-default(type="submit") Login button#login.btn.btn-default(type="submit") Login
.visible-md .visible-md
p#loginform.navbar-text.pull-right 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 &nbsp;&middot;&nbsp; span &nbsp;&middot;&nbsp;
a#register.navbar-link(href="/register") Register a#register.navbar-link(href="/register") Register
@ -69,4 +70,4 @@ mixin navlogoutform(redirect)
p#logoutform.navbar-text.pull-right p#logoutform.navbar-text.pull-right
span#welcome Welcome, #{loginName} span#welcome Welcome, #{loginName}
span &nbsp;&middot;&nbsp; span &nbsp;&middot;&nbsp;
a#logout.navbar-link(href="/logout?dest=#{redirect}") Logout a#logout.navbar-link(href="/logout?dest=#{encodeURIComponent(redirect)}&_csrf=#{csrfToken}") Logout

View File

@ -29,6 +29,7 @@ html(lang="en")
p= registerError p= registerError
h2 Register h2 Register
form(role="form", action="/register", method="post", onsubmit="return verify()") form(role="form", action="/register", method="post", onsubmit="return verify()")
input(type="hidden", name="_csrf", value=csrfToken)
.form-group .form-group
label.control-label(for="username") Username label.control-label(for="username") Username
input#username.form-control(type="text", name="name") input#username.form-control(type="text", name="name")