Skip to content
This repository has been archived by the owner on Jan 17, 2023. It is now read-only.

Commit

Permalink
validate csrf headers
Browse files Browse the repository at this point in the history
  • Loading branch information
Greg Guthe committed Sep 1, 2017
1 parent 17cdcdd commit 8671702
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 16 deletions.
62 changes: 61 additions & 1 deletion server/src/middleware/csrf.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/#";
}

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
Expand Down
4 changes: 2 additions & 2 deletions server/src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -516,7 +516,7 @@ function sendAuthInfo(req, res, params) {
}


app.post("/api/#", function(req, res) {
app.post("/api/#", csrfProtection, function(req, res) {
let vars = req.body;
let deviceInfo = {};
try {
Expand Down
55 changes: 42 additions & 13 deletions test/server/test_csrf.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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

Expand All @@ -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'<input.*name="_csrf".*value="([^"]*)"', page)
Expand All @@ -34,7 +35,7 @@ def test_leave_screenshots_with_invalid_csrftoken_fails():
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)

resp = user.session.post(
urljoin(user.backend, "/leave-screenshots/leave"),
Expand All @@ -48,7 +49,7 @@ def test_leave_screenshots_without_csrftoken_fails():
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)

resp = user.session.post(
urljoin(user.backend, "/leave-screenshots/leave"))
Expand All @@ -61,7 +62,7 @@ def test_leave_screenshots_with_get_fails():
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'<input.*name="_csrf".*value="([^"]*)"', page)
Expand All @@ -76,17 +77,16 @@ def test_leave_screenshots_with_get_fails():
def test_leave_screenshots_with_duplicate_csrf_cookies_fails():
with screenshots_session() as user:
leave_resp = user.session.get(user.backend + "/leave-screenshots/")
print(leave_resp.cookies.get('_csrf'))
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'<input.*name="_csrf".*value="([^"]*)"', page)
csrf = csrf_match.group(1)
resp = user.session.post(
urljoin(user.backend, "/leave-screenshots/leave"),
cookies={'_csrf': leave_resp.cookies.get('_csrf'), # noqa: F601
'_csrf': leave_resp.cookies.get('_csrf')}, # noqa: F601
cookies={'_csrf': user.session.cookies.get('_csrf'), # noqa: F601
'_csrf': user.session.cookies.get('_csrf')}, # noqa: F601
json={"_csrf": csrf})
assert resp.status_code == 400

Expand All @@ -113,13 +113,13 @@ def test_get_shot_sets_csrf_cookie():

resp = user.session.get(shot_url)
resp.raise_for_status()
assert_httponly_csrf_cookie(resp)
assert_httponly_csrf_cookie(user.session)


def test_get_my_shots_sets_csrf_cookie():
with screenshots_session() as user:
resp = user.read_my_shots() # raises on error
assert_httponly_csrf_cookie(resp)
user.read_my_shots() # raises on error
assert_httponly_csrf_cookie(user.session)


def test_delete_shot_with_valid_csrftoken_ok():
Expand Down Expand Up @@ -293,6 +293,33 @@ def test_disconnect_device_without_csrftoken_fails():
assert resp.status_code == 403 # Bad CSRF Token


def test_login_with_invalid_headers():
# might belong in test_auth.py instead
unauthed_user = ScreenshotsClient()
resp = unauthed_user.session.post(
urljoin(unauthed_user.backend, "/api/#"),
headers=dict(origin="https://localhost:8080"),
data=dict(secret=unauthed_user.secret,
deviceInfo=json.dumps(unauthed_user.deviceInfo)))

print resp.text
assert resp.status_code == 403 # Invalid CSRF Headers


def test_register_with_invalid_headers():
# might belong in test_auth.py instead
unauthed_user = ScreenshotsClient()
resp = unauthed_user.session.post(
urljoin(unauthed_user.backend, "/api/register"),
headers=dict(referer="https://localhost:8080/1Zv4srJfp50f5LaJ/localhost"),
data=dict(deviceId=unauthed_user.deviceId,
secret=unauthed_user.secret,
deviceInfo=json.dumps(unauthed_user.deviceInfo)))

print resp.text
assert resp.status_code == 403 # Invalid CSRF Headers


if __name__ == "__main__":
test_leave_screenshots_with_valid_csrftoken_ok()
test_leave_screenshots_with_invalid_csrftoken_fails()
Expand All @@ -317,3 +344,5 @@ def test_disconnect_device_without_csrftoken_fails():
test_disconnect_device_with_valid_csrftoken_ok()
test_disconnect_device_with_invalid_csrftoken_fails()
test_disconnect_device_without_csrftoken_fails()
test_login_with_invalid_headers()
test_register_with_invalid_headers()

0 comments on commit 8671702

Please # to comment.