Added CSRF protection to all API calls. /api/account AJAX calls updated.

This commit is contained in:
rainbow napkin 2024-12-29 21:40:50 -05:00
parent 7e0c8e72c5
commit 106b0fcddb
11 changed files with 149 additions and 14 deletions

View file

@ -0,0 +1,30 @@
/*Canopy - The next generation of stoner streaming software
Copyright (C) 2024-2025 Rainbownapkin and the TTN Community
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.*/
//Config
const config = require('../../config.json');
//Local Imports
const csrfUtils = require('../utils/csrfUtils');
//register page functions
module.exports = async function(req, res, next){
//set status
res.status(404);
//Render page
return res.render('404', {instance: config.instanceName, user: req.session.user, csrfToken: csrfUtils.generateToken(req)});
}

View file

@ -18,7 +18,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
const accountUtils = require('../../../utils/sessionUtils'); const accountUtils = require('../../../utils/sessionUtils');
const {exceptionHandler, errorHandler} = require('../../../utils/loggerUtils'); const {exceptionHandler, errorHandler} = require('../../../utils/loggerUtils');
module.exports.get = async function(req, res){ module.exports.post = async function(req, res){
if(req.session.user){ if(req.session.user){
try{ try{
accountUtils.killSession(req.session); accountUtils.killSession(req.session);

View file

@ -35,7 +35,7 @@ const router = Router();
//login //login
router.post('/login', accountValidator.user(), accountValidator.pass(), loginController.post); router.post('/login', accountValidator.user(), accountValidator.pass(), loginController.post);
//logout //logout
router.get('/logout', logoutController.get); router.post('/logout', logoutController.post);
//register //register
router.post('/register', accountValidator.user(), router.post('/register', accountValidator.user(),
accountValidator.securePass(), accountValidator.securePass(),

View file

@ -21,10 +21,14 @@ const { Router } = require('express');
const accountRouter = require("./api/accountRouter"); const accountRouter = require("./api/accountRouter");
const channelRouter = require("./api/channelRouter"); const channelRouter = require("./api/channelRouter");
const adminRouter = require("./api/adminRouter"); const adminRouter = require("./api/adminRouter");
const csrfUtil = require('../utils/csrfUtils');
//globals //globals
const router = Router(); const router = Router();
//Apply Cross-Site Request Forgery protection to API calls
router.use(csrfUtil.csrfSynchronisedProtection);
//routing functions //routing functions
router.use('/account', accountRouter); router.use('/account', accountRouter);
router.use('/channel', channelRouter); router.use('/channel', channelRouter);

View file

@ -409,11 +409,14 @@ userSchema.methods.getAuthenticatedSessions = async function(){
//crawl through active sessions //crawl through active sessions
sessions.forEach((session) => { sessions.forEach((session) => {
//Skip un-authed sessions
if(session.user != null){
//if a session matches the current user //if a session matches the current user
if(session.user.id == this.id){ if(session.user.id == this.id){
//we return it //we return it
returnArr.push(session); returnArr.push(session);
} }
}
}); });
resolve(returnArr); resolve(returnArr);

View file

@ -32,11 +32,14 @@ const channelManager = require('./app/channel/channelManager');
//Util //Util
const configCheck = require('./utils/configCheck'); const configCheck = require('./utils/configCheck');
const scheduler = require('./utils/scheduler'); const scheduler = require('./utils/scheduler');
const {errorMiddleware} = require('./utils/loggerUtils');
//DB Model //DB Model
const statModel = require('./schemas/statSchema'); const statModel = require('./schemas/statSchema');
const flairModel = require('./schemas/flairSchema'); const flairModel = require('./schemas/flairSchema');
const emoteModel = require('./schemas/emoteSchema'); const emoteModel = require('./schemas/emoteSchema');
const tokeCommandModel = require('./schemas/tokebot/tokeCommandSchema'); const tokeCommandModel = require('./schemas/tokebot/tokeCommandSchema');
//Controller
const fileNotFoundController = require('./controllers/404Controller');
//Router //Router
//Humie-Friendly //Humie-Friendly
const indexRouter = require('./routers/indexRouter'); const indexRouter = require('./routers/indexRouter');
@ -132,6 +135,14 @@ app.use('/lib/altcha',express.static(path.join(__dirname, '../node_modules/altch
//Server public 'www' folder //Server public 'www' folder
app.use(express.static(path.join(__dirname, '../www'))); app.use(express.static(path.join(__dirname, '../www')));
//Handle error checking
app.use(errorMiddleware);
//Basic 404 handler
app.use(fileNotFoundController);
//Increment launch counter //Increment launch counter
statModel.incrementLaunchCount(); statModel.incrementLaunchCount();

View file

@ -17,8 +17,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
//NPM Imports //NPM Imports
const { csrfSync } = require('csrf-sync'); const { csrfSync } = require('csrf-sync');
//Local Imports
const {errorHandler} = require('./loggerUtils');
//Pull needed methods from csrfSync //Pull needed methods from csrfSync
const {generateToken, revokeToken, csrfSynchronisedProtection,} = csrfSync(); const {generateToken, revokeToken, csrfSynchronisedProtection} = csrfSync();
//Export them per csrfSync documentation //Export them per csrfSync documentation
module.exports.generateToken = generateToken; module.exports.generateToken = generateToken;

View file

@ -39,3 +39,16 @@ module.exports.socketCriticalExceptionHandler = function(socket, err){
module.exports.consoleWarn = function(string){ module.exports.consoleWarn = function(string){
console.warn('\x1b[31m\x1b[4m%s\x1b[0m',string); console.warn('\x1b[31m\x1b[4m%s\x1b[0m',string);
} }
//Basic error-handling middleware to ensure we're not dumping stack traces
module.exports.errorMiddleware = function(err, req, res, next){
//Set generic error
var reason = "Server Error";
//If it's un-authorized
if(err.status == 403){
reason = "Unauthorized"
}
module.exports.errorHandler(res, err.message, reason, err.status);
}

33
src/views/404.ejs Normal file
View file

@ -0,0 +1,33 @@
<%# Canopy - The next generation of stoner streaming software
Copyright (C) 2024-2025 Rainbownapkin and the TTN Community
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. %>
<!DOCTYPE html>
<html>
<head>
<%- include('partial/styles', {instance, user}); %>
<%- include('partial/csrfToken', {csrfToken}); %>
<link rel="stylesheet" type="text/css" href="css/error.css">
<title><%= instance %></title>
</head>
<body>
<%- include('partial/navbar', {user}); %>
<h1>404</h1>
<h3>Congratulations, you've found a dead link!</h3>
<img src="https://web.archive.org/web/20091027105418/http://geocities.com/cd_franks_elementary/graphics/cdcongratulationsspinning.gif">
</body>
<footer>
<%- include('partial/scripts', {user}); %>
</footer>
</html>

23
www/css/error.css Normal file
View file

@ -0,0 +1,23 @@
/*Canopy - The next generation of stoner streaming software
Copyright (C) 2024-2025 Rainbownapkin and the TTN Community
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.*/
h1, h3{
text-align: center;
}
img{
width: 50%;
margin: 0 auto;
}

View file

@ -395,7 +395,9 @@ class canopyAjaxUtils{
var response = await fetch(`/api/account/register`,{ var response = await fetch(`/api/account/register`,{
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
//It's either this or find and bind all event listeners :P
"x-csrf-token": utils.ajax.getCSRFToken()
}, },
body: JSON.stringify(email ? {user, pass, passConfirm, email, verification} : {user, pass, passConfirm, verification}) body: JSON.stringify(email ? {user, pass, passConfirm, email, verification} : {user, pass, passConfirm, verification})
}); });
@ -411,7 +413,8 @@ class canopyAjaxUtils{
var response = await fetch(`/api/account/login`,{ var response = await fetch(`/api/account/login`,{
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
}, },
body: JSON.stringify(verification ? {user, pass, verification} : {user, pass}) body: JSON.stringify(verification ? {user, pass, verification} : {user, pass})
}); });
@ -427,7 +430,10 @@ class canopyAjaxUtils{
async logout(){ async logout(){
var response = await fetch(`/api/account/logout`,{ var response = await fetch(`/api/account/logout`,{
method: "GET", method: "POST",
headers: {
"x-csrf-token": utils.ajax.getCSRFToken()
}
}); });
if(response.status == 200){ if(response.status == 200){
@ -441,7 +447,8 @@ class canopyAjaxUtils{
const response = await fetch(`/api/account/update`,{ const response = await fetch(`/api/account/update`,{
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
}, },
body: JSON.stringify(update) body: JSON.stringify(update)
}); });
@ -469,7 +476,8 @@ class canopyAjaxUtils{
const response = await fetch(`/api/account/delete`,{ const response = await fetch(`/api/account/delete`,{
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
}, },
body: JSON.stringify({pass}) body: JSON.stringify({pass})
}); });
@ -485,7 +493,8 @@ class canopyAjaxUtils{
const response = await fetch(`/api/account/passwordResetRequest`,{ const response = await fetch(`/api/account/passwordResetRequest`,{
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
}, },
body: JSON.stringify({user, verification}) body: JSON.stringify({user, verification})
}); });
@ -506,7 +515,8 @@ class canopyAjaxUtils{
const response = await fetch(`/api/account/passwordReset`,{ const response = await fetch(`/api/account/passwordReset`,{
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
}, },
body: JSON.stringify({token, pass, confirmPass, verification}) body: JSON.stringify({token, pass, confirmPass, verification})
}); });
@ -782,6 +792,11 @@ class canopyAjaxUtils{
} }
} }
//Syntatic sugar
getCSRFToken(){
return document.querySelector("[name='csrf-token']").content;
}
} }
const utils = new canopyUtils() const utils = new canopyUtils()