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 {exceptionHandler, errorHandler} = require('../../../utils/loggerUtils');
module.exports.get = async function(req, res){
module.exports.post = async function(req, res){
if(req.session.user){
try{
accountUtils.killSession(req.session);

View file

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

View file

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

View file

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

View file

@ -32,11 +32,14 @@ const channelManager = require('./app/channel/channelManager');
//Util
const configCheck = require('./utils/configCheck');
const scheduler = require('./utils/scheduler');
const {errorMiddleware} = require('./utils/loggerUtils');
//DB Model
const statModel = require('./schemas/statSchema');
const flairModel = require('./schemas/flairSchema');
const emoteModel = require('./schemas/emoteSchema');
const tokeCommandModel = require('./schemas/tokebot/tokeCommandSchema');
//Controller
const fileNotFoundController = require('./controllers/404Controller');
//Router
//Humie-Friendly
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
app.use(express.static(path.join(__dirname, '../www')));
//Handle error checking
app.use(errorMiddleware);
//Basic 404 handler
app.use(fileNotFoundController);
//Increment launch counter
statModel.incrementLaunchCount();

View file

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

View file

@ -39,3 +39,16 @@ module.exports.socketCriticalExceptionHandler = function(socket, err){
module.exports.consoleWarn = function(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`,{
method: "POST",
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})
});
@ -411,7 +413,8 @@ class canopyAjaxUtils{
var response = await fetch(`/api/account/login`,{
method: "POST",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
},
body: JSON.stringify(verification ? {user, pass, verification} : {user, pass})
});
@ -427,7 +430,10 @@ class canopyAjaxUtils{
async logout(){
var response = await fetch(`/api/account/logout`,{
method: "GET",
method: "POST",
headers: {
"x-csrf-token": utils.ajax.getCSRFToken()
}
});
if(response.status == 200){
@ -441,7 +447,8 @@ class canopyAjaxUtils{
const response = await fetch(`/api/account/update`,{
method: "POST",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
},
body: JSON.stringify(update)
});
@ -469,7 +476,8 @@ class canopyAjaxUtils{
const response = await fetch(`/api/account/delete`,{
method: "POST",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
},
body: JSON.stringify({pass})
});
@ -485,7 +493,8 @@ class canopyAjaxUtils{
const response = await fetch(`/api/account/passwordResetRequest`,{
method: "POST",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
},
body: JSON.stringify({user, verification})
});
@ -506,7 +515,8 @@ class canopyAjaxUtils{
const response = await fetch(`/api/account/passwordReset`,{
method: "POST",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
"x-csrf-token": utils.ajax.getCSRFToken()
},
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()