Added CSRF protection to all API calls. /api/account AJAX calls updated.
This commit is contained in:
parent
7e0c8e72c5
commit
106b0fcddb
30
src/controllers/404Controller.js
Normal file
30
src/controllers/404Controller.js
Normal 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)});
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -409,10 +409,13 @@ userSchema.methods.getAuthenticatedSessions = async function(){
|
||||||
|
|
||||||
//crawl through active sessions
|
//crawl through active sessions
|
||||||
sessions.forEach((session) => {
|
sessions.forEach((session) => {
|
||||||
//if a session matches the current user
|
//Skip un-authed sessions
|
||||||
if(session.user.id == this.id){
|
if(session.user != null){
|
||||||
//we return it
|
//if a session matches the current user
|
||||||
returnArr.push(session);
|
if(session.user.id == this.id){
|
||||||
|
//we return it
|
||||||
|
returnArr.push(session);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
33
src/views/404.ejs
Normal 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
23
www/css/error.css
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
Loading…
Reference in a new issue