mirror of
https://github.com/Spengreb/sync.git
synced 2026-06-09 23:02:05 +00:00
Fix CSRF issues from previous commits
This commit is contained in:
parent
2788dae3c8
commit
49623df29d
4 changed files with 162 additions and 20 deletions
|
|
@ -5,22 +5,54 @@
|
|||
import { CSRFError } from '../errors';
|
||||
|
||||
var csrf = require("csrf");
|
||||
var cookieParser = require("cookie-parser");
|
||||
|
||||
var tokens = csrf();
|
||||
|
||||
function getCookieOptions(domain, req) {
|
||||
const secure = req.realProtocol === 'https' || req.secure === true;
|
||||
const options = {
|
||||
signed: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure
|
||||
};
|
||||
|
||||
if (domain && domain.indexOf('.') !== -1 && !/^\d+\.\d+\.\d+\.\d+$/.test(domain)) {
|
||||
options.domain = domain;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function getSignedCSRFCookies(req) {
|
||||
var header = req.headers && req.headers.cookie;
|
||||
if (!header || !req.secret) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return header.split(';')
|
||||
.map(part => part.trim())
|
||||
.filter(part => part.indexOf('_csrf=') === 0)
|
||||
.map(part => part.slice('_csrf='.length))
|
||||
.map(value => {
|
||||
try {
|
||||
return decodeURIComponent(value);
|
||||
} catch (_err) {
|
||||
return value;
|
||||
}
|
||||
})
|
||||
.filter(value => value.indexOf('s:') === 0)
|
||||
.map(value => cookieParser.signedCookie(value, req.secret))
|
||||
.filter(value => typeof value === 'string');
|
||||
}
|
||||
|
||||
exports.init = function csrfInit (domain) {
|
||||
return function (req, res, next) {
|
||||
var secret = req.signedCookies._csrf;
|
||||
if (!secret) {
|
||||
secret = tokens.secretSync();
|
||||
const secure = req.realProtocol === 'https' || req.secure === true;
|
||||
res.cookie("_csrf", secret, {
|
||||
domain: domain,
|
||||
signed: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure
|
||||
});
|
||||
res.cookie("_csrf", secret, getCookieOptions(domain, req));
|
||||
}
|
||||
|
||||
var token;
|
||||
|
|
@ -39,12 +71,16 @@ exports.init = function csrfInit (domain) {
|
|||
};
|
||||
|
||||
exports.verify = function csrfVerify(req) {
|
||||
var secret = req.signedCookies._csrf;
|
||||
var secrets = getSignedCSRFCookies(req);
|
||||
if (req.signedCookies._csrf) {
|
||||
secrets.unshift(req.signedCookies._csrf);
|
||||
}
|
||||
|
||||
var token = (req.body && req.body._csrf) ||
|
||||
(req.query && req.query._csrf) ||
|
||||
req.header('x-csrf-token');
|
||||
|
||||
if (!tokens.verify(secret, token)) {
|
||||
if (!secrets.some(secret => tokens.verify(secret, token))) {
|
||||
throw new CSRFError('Invalid CSRF token');
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
const express = require('express');
|
||||
const csrf = require('../../csrf');
|
||||
const LOGGER = require('@calzoneman/jsli')('web/api/csrf');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
|
@ -12,6 +13,32 @@ function hasBotBearerToken(req) {
|
|||
return typeof authHeader === 'string' && /^Bearer\s+cbt_/.test(authHeader);
|
||||
}
|
||||
|
||||
function hasSameOriginHeaders(req) {
|
||||
const host = req.header('host');
|
||||
if (!host) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const origin = req.header('origin');
|
||||
if (typeof origin === 'string' && origin.length > 0) {
|
||||
return origin.indexOf('://' + host) !== -1;
|
||||
}
|
||||
|
||||
const referer = req.header('referer');
|
||||
if (typeof referer === 'string' && referer.length > 0) {
|
||||
return referer.indexOf('://' + host + '/') !== -1 || referer.endsWith('://' + host);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function canUseXHRCompatFallback(req) {
|
||||
const xhrHeader = String(req.header('x-requested-with') || '');
|
||||
return xhrHeader.toLowerCase() === 'xmlhttprequest' &&
|
||||
Boolean(req.signedCookies && req.signedCookies._csrf) &&
|
||||
hasSameOriginHeaders(req);
|
||||
}
|
||||
|
||||
router.use((req, res, next) => {
|
||||
if (!isMutatingMethod(req.method) || hasBotBearerToken(req)) {
|
||||
return next();
|
||||
|
|
@ -21,6 +48,26 @@ router.use((req, res, next) => {
|
|||
csrf.verify(req);
|
||||
next();
|
||||
} catch (_err) {
|
||||
if (canUseXHRCompatFallback(req)) {
|
||||
LOGGER.warn(
|
||||
'CSRF compat fallback accepted %s %s',
|
||||
req.method,
|
||||
req.originalUrl || req.url
|
||||
);
|
||||
return next();
|
||||
}
|
||||
|
||||
LOGGER.warn(
|
||||
'CSRF reject %s %s authHeader=%s csrfHeader=%s bodyToken=%s queryToken=%s signedCsrfCookie=%s rawCookieHasCsrf=%s',
|
||||
req.method,
|
||||
req.originalUrl || req.url,
|
||||
Boolean(req.headers && req.headers.authorization),
|
||||
Boolean(req.header('x-csrf-token')),
|
||||
Boolean(req.body && req.body._csrf),
|
||||
Boolean(req.query && req.query._csrf),
|
||||
Boolean(req.signedCookies && req.signedCookies._csrf),
|
||||
Boolean(req.headers && req.headers.cookie && req.headers.cookie.indexOf('_csrf=') !== -1)
|
||||
);
|
||||
return res.status(403).json({ error: 'Invalid CSRF token' });
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -253,7 +253,7 @@ html(lang="en")
|
|||
script(src="/js/tabcomplete.js")
|
||||
script(src="/js/player.js")
|
||||
script(src="/js/paginator.js")
|
||||
script(src="/js/ui.js")
|
||||
script(src="/js/ui.js?v=csrf-api-v2")
|
||||
script(src="/js/callbacks.js")
|
||||
script(defer, src="/js/vjs/dash.all.min.js")
|
||||
script(defer, src="/js/vjs/video.js")
|
||||
|
|
|
|||
77
www/js/ui.js
77
www/js/ui.js
|
|
@ -1257,13 +1257,14 @@ $("#resize-video-smaller").on('click', function () {
|
|||
}
|
||||
});
|
||||
|
||||
$.ajaxPrefilter(function (options, _originalOptions, _jqXHR) {
|
||||
$.ajaxPrefilter(function (options, originalOptions, _jqXHR) {
|
||||
var url = String(options.url || '');
|
||||
if (!/\/api\/v1\//.test(url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var method = String(options.type || options.method || 'GET').toUpperCase();
|
||||
var requestedMethod = originalOptions && (originalOptions.method || originalOptions.type);
|
||||
var method = String(requestedMethod || options.method || options.type || 'GET').toUpperCase();
|
||||
if (method === 'GET' || method === 'HEAD' || method === 'OPTIONS') {
|
||||
return;
|
||||
}
|
||||
|
|
@ -1275,10 +1276,59 @@ $.ajaxPrefilter(function (options, _originalOptions, _jqXHR) {
|
|||
|
||||
if (typeof CSRF_TOKEN === 'string' && CSRF_TOKEN.length > 0) {
|
||||
options.headers['X-CSRF-Token'] = CSRF_TOKEN;
|
||||
|
||||
var hasCSRFInURL = /(?:\?|&)_csrf=/.test(url);
|
||||
var contentType = String(options.contentType || '').toLowerCase();
|
||||
var data = options.data;
|
||||
|
||||
if (method === 'DELETE' && (data === undefined || data === null || data === '') && !hasCSRFInURL) {
|
||||
options.url = url + (url.indexOf('?') === -1 ? '?' : '&') +
|
||||
'_csrf=' + encodeURIComponent(CSRF_TOKEN);
|
||||
return;
|
||||
}
|
||||
|
||||
if (contentType.indexOf('application/json') === 0) {
|
||||
var obj = null;
|
||||
if (typeof data === 'string' && data.length > 0) {
|
||||
try {
|
||||
obj = JSON.parse(data);
|
||||
} catch (_err) {
|
||||
obj = null;
|
||||
}
|
||||
} else if (typeof data === 'object' && data !== null) {
|
||||
obj = data;
|
||||
}
|
||||
if (obj && typeof obj === 'object' && !Object.prototype.hasOwnProperty.call(obj, '_csrf')) {
|
||||
obj._csrf = CSRF_TOKEN;
|
||||
options.data = JSON.stringify(obj);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof data === 'string') {
|
||||
if (!/(?:^|&)_csrf=/.test(data)) {
|
||||
options.data = data + (data.length > 0 ? '&' : '') +
|
||||
'_csrf=' + encodeURIComponent(CSRF_TOKEN);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof data === 'object' && data !== null && !Object.prototype.hasOwnProperty.call(data, '_csrf')) {
|
||||
data._csrf = CSRF_TOKEN;
|
||||
return;
|
||||
}
|
||||
|
||||
if (data === undefined || data === null) {
|
||||
options.data = { _csrf: CSRF_TOKEN };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var CSTBots = (function () {
|
||||
function csrfField() {
|
||||
return (typeof CSRF_TOKEN === 'string' && CSRF_TOKEN.length > 0) ? CSRF_TOKEN : '';
|
||||
}
|
||||
|
||||
function apiBase() {
|
||||
return '/api/v1/channels/' + CHANNEL.name;
|
||||
}
|
||||
|
|
@ -1315,7 +1365,8 @@ var CSTBots = (function () {
|
|||
if (!confirm('Revoke this bot token? Any connected bot will be disconnected immediately.')) return;
|
||||
$.ajax({
|
||||
url: apiBase() + '/bots/' + id,
|
||||
method: 'DELETE'
|
||||
method: 'DELETE',
|
||||
data: { _csrf: csrfField() }
|
||||
}).done(function () {
|
||||
load();
|
||||
}).fail(function (xhr) {
|
||||
|
|
@ -1331,7 +1382,7 @@ var CSTBots = (function () {
|
|||
url: apiBase() + '/bots',
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ name: name, rank: rank })
|
||||
data: JSON.stringify({ name: name, rank: rank, _csrf: csrfField() })
|
||||
}).done(function (data) {
|
||||
$('#cs-bots-token-value').text(data.token);
|
||||
$('.cs-bots-token-result').show();
|
||||
|
|
@ -1357,6 +1408,10 @@ var CSTBots = (function () {
|
|||
})();
|
||||
|
||||
var CSTShows = (function () {
|
||||
function csrfField() {
|
||||
return (typeof CSRF_TOKEN === 'string' && CSRF_TOKEN.length > 0) ? CSRF_TOKEN : '';
|
||||
}
|
||||
|
||||
var selectedId = null;
|
||||
var draftPlaylist = [];
|
||||
var timezoneOptionsLoaded = false;
|
||||
|
|
@ -1471,7 +1526,7 @@ var CSTShows = (function () {
|
|||
url: apiBase() + '/resolve-media',
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ items: unresolved })
|
||||
data: JSON.stringify({ items: unresolved, _csrf: csrfField() })
|
||||
}).done(function (data) {
|
||||
var map = {};
|
||||
(data.items || []).forEach(function (item) {
|
||||
|
|
@ -1620,7 +1675,7 @@ var CSTShows = (function () {
|
|||
url: apiBase() + '/' + id + '/action',
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ action: actionName })
|
||||
data: JSON.stringify({ action: actionName, _csrf: csrfField() })
|
||||
}).done(function () {
|
||||
load();
|
||||
}).fail(function (xhr) {
|
||||
|
|
@ -1661,7 +1716,11 @@ var CSTShows = (function () {
|
|||
$('<button class=\"btn btn-xs btn-danger\">Delete</button>')
|
||||
.on('click', function () {
|
||||
if (!confirm('Delete this show?')) return;
|
||||
$.ajax({ url: apiBase() + '/' + show.id, method: 'DELETE' })
|
||||
$.ajax({
|
||||
url: apiBase() + '/' + show.id,
|
||||
method: 'DELETE',
|
||||
data: { _csrf: csrfField() }
|
||||
})
|
||||
.done(load)
|
||||
.fail(function (xhr) {
|
||||
alert('Delete failed: ' + ((xhr.responseJSON && xhr.responseJSON.error) || xhr.statusText));
|
||||
|
|
@ -1686,7 +1745,7 @@ var CSTShows = (function () {
|
|||
url: apiBase(),
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(payload)
|
||||
data: JSON.stringify(Object.assign({}, payload, { _csrf: csrfField() }))
|
||||
}).done(function () {
|
||||
clearForm();
|
||||
load();
|
||||
|
|
@ -1707,7 +1766,7 @@ var CSTShows = (function () {
|
|||
url: apiBase() + '/' + selectedId,
|
||||
method: 'PUT',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(payload)
|
||||
data: JSON.stringify(Object.assign({}, payload, { _csrf: csrfField() }))
|
||||
}).done(function () {
|
||||
load();
|
||||
}).fail(function (xhr) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue