Protect /api/v1 mutations with CSRF for cookie auth while exempting cbt_ bearer bot tokens and wiring UI X-CSRF-Token headers

This commit is contained in:
Speng Reb 2026-05-21 16:23:30 +02:00
parent 12696452aa
commit 6eeee342d7
4 changed files with 49 additions and 1 deletions

View file

@ -37,7 +37,9 @@ exports.init = function csrfInit (domain) {
exports.verify = function csrfVerify(req) {
var secret = req.signedCookies._csrf;
var token = req.body._csrf || req.query._csrf;
var token = (req.body && req.body._csrf) ||
(req.query && req.query._csrf) ||
req.header('x-csrf-token');
if (!tokens.verify(secret, token)) {
throw new CSRFError('Invalid CSRF token');

View file

@ -1,7 +1,30 @@
const express = require('express');
const csrf = require('../../csrf');
const router = express.Router();
function isMutatingMethod(method) {
return method === 'POST' || method === 'PUT' || method === 'PATCH' || method === 'DELETE';
}
function hasBotBearerToken(req) {
const authHeader = req.headers && req.headers.authorization;
return typeof authHeader === 'string' && /^Bearer\s+cbt_/.test(authHeader);
}
router.use((req, res, next) => {
if (!isMutatingMethod(req.method) || hasBotBearerToken(req)) {
return next();
}
try {
csrf.verify(req);
next();
} catch (_err) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
});
router.use('/channels/:channel/bots', require('./bots'));
router.use('/channels/:channel/emotes', require('./emotes'));
router.use('/channels/:channel/playlist', require('./playlist'));

View file

@ -15,10 +15,12 @@ mixin head()
var DEFAULT_THEME = '#{DEFAULT_THEME}';
var CHANNELPATH = '#{channelPath}';
var CHANNELNAME = '#{channelName}';
var CSRF_TOKEN = '#{csrfToken}';
else
script(type="text/javascript").
var DEFAULT_THEME = '#{DEFAULT_THEME}';
var CHANNELPATH = '#{channelPath}';
var CSRF_TOKEN = '#{csrfToken}';
script(src="/js/theme.js")
//[if lt IE 9]

View file

@ -1257,6 +1257,27 @@ $("#resize-video-smaller").on('click', function () {
}
});
$.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();
if (method === 'GET' || method === 'HEAD' || method === 'OPTIONS') {
return;
}
options.headers = options.headers || {};
if (options.headers.Authorization || options.headers.authorization) {
return;
}
if (typeof CSRF_TOKEN === 'string' && CSRF_TOKEN.length > 0) {
options.headers['X-CSRF-Token'] = CSRF_TOKEN;
}
});
var CSTBots = (function () {
function apiBase() {
return '/api/v1/channels/' + CHANNEL.name;