From 8671702cd7c7c1d207a014f242bdfcdca5f3a16c Mon Sep 17 00:00:00 2001 From: Greg Guthe Date: Thu, 31 Aug 2017 15:25:35 -0400 Subject: [PATCH] validate csrf headers --- server/src/middleware/csrf.js | 62 ++++++++++++++++++++++++++++++++++- server/src/server.js | 4 +-- test/server/test_csrf.py | 55 +++++++++++++++++++++++-------- 3 files changed, 105 insertions(+), 16 deletions(-) diff --git a/server/src/middleware/csrf.js b/server/src/middleware/csrf.js index 029e3bbbeb..3e2e5ff03b 100644 --- a/server/src/middleware/csrf.js +++ b/server/src/middleware/csrf.js @@ -8,7 +8,67 @@ const config = require("../config").getProperties(); const useSecureCsrfCookie = (config.expectProtocol && /^https$/.test(config.expectProtocol)); -exports.csrfProtection = csrf({cookie: {httpOnly: true, secure: useSecureCsrfCookie}}); +const csrfMiddleware = csrf({ + cookie: {httpOnly: true, secure: useSecureCsrfCookie} +}); + +const csrfExemptMiddleware = csrf({ + ignoreMethods: ["PATCH", "POST", "PUT"], + cookie: {httpOnly: true, secure: useSecureCsrfCookie} +}); + + +function isAuthPath(path) { + return path === "/api/register" || path === "/api/login"; +} + +function isCsrfExemptPath(path) { + return isAuthPath(path) + || path.startsWith("/data") + || path === "/event" + || path === "/error"; +} + +function csrfHeadersValid(req) { + const origin = req.headers.origin; + const referer = req.headers.referer; + + if (isAuthPath(req.path)) { + // web ext background scripts don't send headers + return origin === undefined && referer === undefined; + } + + return true; +} + +function csrfInvalidHeaderResponse(req, res) { + mozlog.warn("bad-csrf-headers", {ip: req.ip, url: req.url, origin: req.headers.origin, referer: req.headers.referer}); + res.status(403); + res.type("text"); + res.send("Invalid CSRF Headers"); +} + +const ignoreMethods = { + "GET": true, + "HEAD": true, + "OPTIONS": true +}; + +exports.csrfProtection = function(req, res, next) { + // check origin and referer headers + if (!(ignoreMethods[req.method.toUpperCase()] || csrfHeadersValid(req))) { + csrfInvalidHeaderResponse(req, res); + return; + } + + if (isCsrfExemptPath(req.path)) { + // just set csrf cookie and attach req.csrfToken + csrfExemptMiddleware(req, res, next); + return; + } + // also validate csrf token for unignored http methods + csrfMiddleware(req, res, next); +}; exports.csrf = function(req, res, next) { // The cookies library doesn't detect duplicates; check manually diff --git a/server/src/server.js b/server/src/server.js index b49f2cfbc6..aed85226d6 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -449,7 +449,7 @@ app.post("/event", function(req, res) { }); }); -app.post("/api/register", function(req, res) { +app.post("/api/register", csrfProtection, function(req, res) { let vars = req.body; let canUpdate = vars.deviceId === req.deviceId; if (!vars.deviceId) { @@ -516,7 +516,7 @@ function sendAuthInfo(req, res, params) { } -app.post("/api/login", function(req, res) { +app.post("/api/login", csrfProtection, function(req, res) { let vars = req.body; let deviceInfo = {}; try { diff --git a/test/server/test_csrf.py b/test/server/test_csrf.py index aef95f1260..aa3a80017b 100644 --- a/test/server/test_csrf.py +++ b/test/server/test_csrf.py @@ -1,5 +1,6 @@ -from clientlib import screenshots_session +from clientlib import ScreenshotsClient, screenshots_session from urlparse import urljoin, urlsplit +import json import random import re @@ -10,7 +11,7 @@ def assert_httponly_csrf_cookie(response, cookie_name='_csrf'): "Test helper" - assert response.cookies.get(cookie_name) # cookie exits + assert response.cookies.get(cookie_name) # cookie exists csrf_cookie = [c for c in response.cookies if c.name == cookie_name][0] assert csrf_cookie.has_nonstandard_attr('HttpOnly') # is HttpOnly @@ -19,7 +20,7 @@ def test_leave_screenshots_with_valid_csrftoken_ok(): with screenshots_session() as user: leave_resp = user.session.get(user.backend + "/leave-screenshots/") assert leave_resp.status_code == 200 - assert_httponly_csrf_cookie(leave_resp) + assert_httponly_csrf_cookie(user.session) page = leave_resp.text csrf_match = re.search(r'