diff --git a/src/web/csrf.js b/src/web/csrf.js index 2fd6fa66..9c2e8cd3 100644 --- a/src/web/csrf.js +++ b/src/web/csrf.js @@ -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'); diff --git a/src/web/routes/api/index.js b/src/web/routes/api/index.js index 79d8aee4..044c1b62 100644 --- a/src/web/routes/api/index.js +++ b/src/web/routes/api/index.js @@ -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')); diff --git a/templates/head.pug b/templates/head.pug index 597e977b..df82619b 100644 --- a/templates/head.pug +++ b/templates/head.pug @@ -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] diff --git a/www/js/ui.js b/www/js/ui.js index 665fe1b0..ae14b22a 100644 --- a/www/js/ui.js +++ b/www/js/ui.js @@ -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;