xsshunter/api.js

620 lines
18 KiB
JavaScript
Raw Normal View History

2021-05-31 19:06:40 +00:00
const bcrypt = require('bcrypt');
const express = require('express');
const cors = require('cors');
const path = require('path');
const uuid = require('uuid');
const asyncfs = require('fs').promises;
const sessions = require('@nvanexan/node-client-sessions');
const favicon = require('serve-favicon');
const database = require('./database.js');
2023-01-16 02:37:17 +00:00
const Users = database.Users;
2023-01-16 04:52:26 +00:00
const Secrets = database.Secrets;
2021-05-31 19:06:40 +00:00
const safeCompare = require('safe-compare');
const { Op } = require("sequelize");
const PayloadFireResults = database.PayloadFireResults;
const CollectedPages = database.CollectedPages;
const InjectionRequests = database.InjectionRequests;
const constants = require('./constants.js');
const validate = require('express-jsonschema').validate;
const get_hashed_password = require('./utils.js').get_hashed_password;
const get_secure_random_string = require('./utils.js').get_secure_random_string;
2023-01-16 02:34:56 +00:00
const {google} = require('googleapis');
const {OAuth2Client} = require('google-auth-library');
2021-05-31 19:06:40 +00:00
const SCREENSHOTS_DIR = path.resolve(process.env.SCREENSHOTS_DIR);
2023-01-16 02:34:56 +00:00
const client = new OAuth2Client(process.env.CLIENT_ID, process.env.CLIENT_SECRET, `https://${process.env.HOSTNAME}/oauth-login`);
2021-05-31 19:06:40 +00:00
var sessions_middleware = false;
var sessions_settings_object = {
cookieName: 'session',
duration: 7 * 24 * 60 * 60 * 1000, // Default session time is a week
activeDuration: 1000 * 60 * 5, // Extend for five minutes if actively used
cookie: {
httpOnly: true,
secure: true
}
}
2023-01-16 03:16:35 +00:00
function makeRandomPath(length) {
var result = '';
var characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for ( var i = 0; i < length; i++ ) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
2021-05-31 19:06:40 +00:00
function session_wrapper_function(req, res, next) {
return sessions_middleware(req, res, next);
}
async function set_up_api_server(app) {
// Check for existing session secret value
2023-01-17 04:35:16 +00:00
const session_secret_setting = process.env.SESSION_SECRET_KEY;
2021-05-31 19:06:40 +00:00
if (!session_secret_setting) {
console.error(`No session secret is set, can't start API server (this really shouldn't happen...)!`);
throw new Error('NO_SESSION_SECRET_SET');
return
}
const updated_session_settings = {
...sessions_settings_object,
...{
2023-01-17 05:13:21 +00:00
secret: session_secret_setting
2021-05-31 19:06:40 +00:00
}
};
sessions_middleware = sessions(updated_session_settings);
// Session management
app.use(session_wrapper_function);
2023-01-16 08:11:30 +00:00
/* lol make this be a thing later TODO
2023-01-16 08:03:35 +00:00
// Limit how big uploads are
app.use(fileUpload({
limits: {
fileSize: 2000000 //2mb
},
abortOnLimit: true
}));
2023-01-16 08:11:30 +00:00
*/
2023-01-16 08:03:35 +00:00
2021-05-31 19:06:40 +00:00
// If that's not present, the request should be rejected.
app.use(async function(req, res, next) {
// Must be an API route else CSRF protection doesn't matter
if(!req.path.startsWith(constants.API_BASE_PATH)) {
next();
return
}
// Check to see if the required CSRF header is set
// If it's not set, reject the request.
const csrf_header_value = req.header(constants.csrf_header_name);
if(!csrf_header_value) {
res.status(401).json({
"success": false,
"error": "No CSRF header specified, request rejected.",
"code": "CSRF_VIOLATION"
}).end();
return
}
// Otherwise we're fine to continue
next();
});
// Restrict all API routes unless the user is authenticated.
app.use(async function(req, res, next) {
const AUTHENTICATION_REQUIRED_ROUTES = [
constants.API_BASE_PATH + 'payloadfires',
constants.API_BASE_PATH + 'collected_pages',
constants.API_BASE_PATH + 'settings',
2023-01-17 04:35:16 +00:00
constants.API_BASE_PATH + 'xss-uri',
constants.API_BASE_PATH + 'user-path',
2021-05-31 19:06:40 +00:00
];
2021-09-17 05:43:16 +00:00
// Check if the path being accessed required authentication
var requires_authentication = false;
AUTHENTICATION_REQUIRED_ROUTES.map(authenticated_route => {
if(req.path.toLowerCase().startsWith(authenticated_route)) {
requires_authentication = true;
}
});
2021-05-31 19:06:40 +00:00
// If the route is not one of the authentication required routes
// then we can allow it through.
2021-09-17 05:43:16 +00:00
if(!requires_authentication) {
2021-05-31 19:06:40 +00:00
next();
return;
}
// If the user is authenticated, let them pass
if(req.session.authenticated === true) {
next();
return;
}
// Otherwise, fall to blocking them by default.
res.status(401).json({
"success": false,
"error": "You must be authenticated to use this endpoint.",
"code": "NOT_AUTHENTICATED"
}).end();
return
});
2023-01-16 02:34:56 +00:00
app.get('/login', (req, res) => {
const authUrl = client.generateAuthUrl({
redirect_uri: `https://${process.env.HOSTNAME}/oauth-login`,
access_type: 'offline',
scope: ['email', 'profile'],
prompt: 'select_account'
});
res.redirect(authUrl);
});
app.get('/oauth-login', async (req, res) => {
try{
const code = req.query.code;
const {tokens} = await client.getToken(code);
client.setCredentials(tokens);
const oauth2 = google.oauth2({version: 'v2', auth: client});
const googleUserProfile = await oauth2.userinfo.v2.me.get();
const email = googleUserProfile.data.email
const [user, created] = await Users.findOrCreate({ where: { 'email': email } });
if(created){
user.path = makeRandomPath(20);
2023-01-17 05:02:46 +00:00
user.injectionCorrelationAPIKey = makeRandomPath(20);
2023-01-16 02:34:56 +00:00
user.save();
}
req.session.email = user.email;
2023-01-16 04:49:17 +00:00
req.session.user_id = user.id;
2023-01-16 02:34:56 +00:00
req.session.authenticated = true;
2023-01-16 04:20:14 +00:00
res.redirect("/app/");
2023-01-16 02:34:56 +00:00
} catch (error) {
console.log(`Error Occured: ${error}`);
res.status(500).send("Error Occured");
}
});
2021-05-31 19:06:40 +00:00
// Serve the front-end
2023-01-16 04:12:04 +00:00
app.use('/app/', express.static(
2021-05-31 19:06:40 +00:00
'./front-end/dist/',
{
setHeaders: function (res, path, stat) {
res.set("Content-Security-Policy", "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; font-src 'self'; connect-src 'self'; prefetch-src 'self'; manifest-src 'self'");
},
},
));
app.use(favicon('./front-end/dist/favicon.ico'));
/*
Endpoint which returns if the user is logged in or not.
*/
app.get(constants.API_BASE_PATH + 'auth-check', async (req, res) => {
res.status(200).json({
"success": true,
"result": {
"is_authenticated": (req.session.authenticated == true)
}
}).end();
});
2023-01-16 05:26:08 +00:00
/*
Just returns the user's XSS URI if logged in.
*/
app.get(constants.API_BASE_PATH + 'xss-uri', async (req, res) => {
const user = await Users.findOne({ where: { 'id': req.session.user_id } });
const uri = process.env.XSS_HOSTNAME + "/" + user.path;
res.status(200).json({
"success": true,
"result": {
"uri": uri
}
}).end();
});
2023-01-16 06:13:54 +00:00
/*
Get the user's path.
*/
app.get(constants.API_BASE_PATH + 'user-path', async (req, res) => {
const user = await Users.findOne({ where: { 'id': req.session.user_id } });
res.status(200).json({
"success": true,
"result": {
2023-01-16 06:57:31 +00:00
"path": user.path
2023-01-16 06:13:54 +00:00
}
}).end();
});
/*
Update the user's path.
*/
app.put(constants.API_BASE_PATH + 'user-path', async (req, res) => {
2023-01-16 07:16:39 +00:00
let collisionUser;
2023-01-16 07:18:31 +00:00
let desiredPath;
2023-01-16 07:10:59 +00:00
if(typeof req.body.user_path == 'string'){
2023-01-16 07:18:31 +00:00
desiredPath = req.body.user_path;
2023-01-16 07:16:39 +00:00
collisionUser = await Users.findOne({ where: { 'path': desiredPath } });
2023-01-16 06:13:54 +00:00
}else{
return res.status(200).json({
"success": false,
"error": "invalid path"
}).end();
}
if( collisionUser ){
return res.status(200).json({
"success": false,
"error": "Path taken by another user"
}).end();
}
const user = await Users.findOne({ where: { 'id': req.session.user_id } });
user.path = desiredPath;
user.save();
res.status(200).json({
"success": true,
"result": {
2023-01-16 06:57:31 +00:00
"path": user.path
2023-01-16 06:13:54 +00:00
}
}).end();
});
2023-01-16 05:26:08 +00:00
2021-05-31 19:06:40 +00:00
/*
Attempt to log into the administrator account
*/
const LoginSchema = {
type: 'object',
properties: {
password: {
type: 'string',
minLength: 1,
maxLength: 72,
required: true,
},
}
}
/*
Deletes a given XSS payload(s)
*/
const DeletePayloadFiresSchema = {
type: 'object',
properties: {
ids: {
type: 'array',
required: true,
items: {
type: 'string'
}
}
}
}
app.delete(constants.API_BASE_PATH + 'payloadfires', validate({ body: DeletePayloadFiresSchema }), async (req, res) => {
const ids_to_delete = req.body.ids;
// Pull the corresponding screenshot_ids from the DB so
// we can delete all the payload fire images as well as
// the payload records themselves.
const screenshot_id_records = await PayloadFireResults.findAll({
where: {
id: {
[Op.in]: ids_to_delete
2023-01-16 04:49:17 +00:00
},
user_id: req.session.user_id
2021-05-31 19:06:40 +00:00
},
attributes: ['id', 'screenshot_id']
});
const screenshots_to_delete = screenshot_id_records.map(payload => {
return `${SCREENSHOTS_DIR}/${payload.screenshot_id}.png.gz`;
});
await Promise.all(screenshots_to_delete.map(screenshot_path => {
return asyncfs.unlink(screenshot_path);
}));
const payload_fires = await PayloadFireResults.destroy({
where: {
id: {
[Op.in]: ids_to_delete
}
}
});
res.status(200).json({
'success': true,
'result': {}
}).end();
});
/*
Returns the list of XSS payload fire results.
*/
const ListPayloadFiresSchema = {
type: 'object',
properties: {
page: {
type: 'string',
required: false,
default: '0',
pattern: '[0-9]+',
},
limit: {
type: 'string',
required: false,
default: '10',
pattern: '[0-9]+',
},
}
}
app.get(constants.API_BASE_PATH + 'payloadfires', validate({ query: ListPayloadFiresSchema }), async (req, res) => {
const page = (parseInt(req.query.page) - 1);
const limit = parseInt(req.query.limit);
const offset = (page * limit);
const payload_fires = await PayloadFireResults.findAndCountAll({
2023-01-16 04:49:17 +00:00
where: {
user_id: req.session.user_id
2023-01-16 04:50:44 +00:00
},
2023-01-16 04:49:17 +00:00
limit: limit,
2021-05-31 19:06:40 +00:00
offset: (page * limit),
order: [['createdAt', 'DESC']],
});
2023-01-16 04:49:17 +00:00
let return_payloads = [];
for(let payload of payload_fires.rows){
let secrets = await Secrets.findAndCountAll({
where: {
payload_id: payload.id
}
});
2023-01-17 06:02:21 +00:00
payload.secrets = [];
for(let secret of secrets.rows){
payload.secrets.push(secret);
}
2023-01-16 04:49:17 +00:00
return_payloads.push(payload);
}
2021-05-31 19:06:40 +00:00
res.status(200).json({
'success': true,
'result': {
2023-01-16 04:49:17 +00:00
'payload_fires': return_payloads,
2021-05-31 19:06:40 +00:00
'total': payload_fires.count
}
}).end();
});
/*
Returns the list of collected pages
*/
const ListCollectedPagesSchema = {
type: 'object',
properties: {
page: {
type: 'string',
required: false,
default: '0',
pattern: '[0-9]+',
},
limit: {
type: 'string',
required: false,
default: '10',
pattern: '[0-9]+',
},
}
}
app.get(constants.API_BASE_PATH + 'collected_pages', validate({ query: ListCollectedPagesSchema }), async (req, res) => {
const page = (parseInt(req.query.page) - 1);
const limit = parseInt(req.query.limit);
const offset = (page * limit);
const collected_pages = await CollectedPages.findAndCountAll({
2023-01-16 04:49:17 +00:00
where: {
user_id: req.session.user_id
2023-01-16 04:50:44 +00:00
},
2021-05-31 19:06:40 +00:00
limit: limit,
offset: (page * limit),
order: [['createdAt', 'DESC']],
});
res.status(200).json({
'success': true,
'result': {
'collected_pages': collected_pages.rows,
'total': collected_pages.count
}
}).end();
});
/*
Deletes a given collected page(s)
*/
const DeleteCollectedPagesSchema = {
type: 'object',
properties: {
ids: {
type: 'array',
required: true,
items: {
type: 'string'
}
}
}
}
app.delete(constants.API_BASE_PATH + 'collected_pages', validate({ body: DeleteCollectedPagesSchema }), async (req, res) => {
const ids_to_delete = req.body.ids;
const payload_fires = await CollectedPages.destroy({
where: {
id: {
[Op.in]: ids_to_delete
}
}
});
res.status(200).json({
'success': true,
'result': {}
}).end();
});
/*
Correlated injections API endpoint.
Authentication is custom for this endpoint
(Uses the correlation API key)
*/
const RecordCorrelatedRequestSchema = {
type: 'object',
properties: {
request: {
type: 'string',
required: true,
},
owner_correlation_key: {
type: 'string',
required: true,
},
injection_key: {
type: 'string',
required: true,
},
}
}
app.post(constants.API_BASE_PATH + 'record_injection', validate({ body: RecordCorrelatedRequestSchema }), async (req, res) => {
2023-01-17 04:35:16 +00:00
const user = await Users.findOne({
2021-05-31 19:06:40 +00:00
where: {
2023-01-17 04:35:16 +00:00
injectionCorrelationAPIKey: req.body.owner_correlation_key
2021-05-31 19:06:40 +00:00
}
});
2023-01-17 04:35:16 +00:00
if (! user) {
2021-05-31 19:06:40 +00:00
res.status(200).json({
"success": false,
"error": "Invalid authentication provided. Please provide a proper correlation API key.",
"code": "INVALID_CREDENTIALS"
}).end();
return
}
try {
// Create injection correlation record
await InjectionRequests.create({
id: uuid.v4(),
request: req.body.request,
injection_key: req.body.injection_key,
});
} catch (e) {
if(e.name === 'SequelizeUniqueConstraintError') {
res.status(200).json({
"success": false,
"error": "That injection key has already been used previously.",
"code": "EXISTING_INJECTION_KEY"
}).end();
return
}
res.status(200).json({
"success": false,
"error": "An unexpected error occurred.",
"code": e.name.toString(),
}).end();
return
}
res.status(200).json({
"success": true,
"message": "Injection request successfully recorded!"
}).end();
});
/*
Returns current settings values for the UI
*/
app.get(constants.API_BASE_PATH + 'settings', async (req, res) => {
2023-01-17 04:35:16 +00:00
let returnObj = {}
const user = await Users.findOne({ where: { 'id': req.session.user_id } });
if(! user){
return res.send("Invalid");
}
returnObj.correlation_api_key = user.injectionCorrelationAPIKey;
returnObj.chainload_uri = user.additionalJS;
returnObj.send_alert_emails = user.sendEmailAlerts;
2021-05-31 19:06:40 +00:00
res.status(200).json({
'success': true,
2023-01-17 05:35:56 +00:00
'result': returnObj
2021-05-31 19:06:40 +00:00
}).end();
});
/*
Updates a specific config for the service
*/
const UpdateConfigSchema = {
type: 'object',
properties: {
password: {
type: 'string',
required: false,
},
correlation_api_key: {
type: 'boolean',
required: false,
},
chainload_uri: {
type: 'string',
required: false,
},
send_alert_emails: {
type: 'boolean',
required: false,
},
revoke_all_sessions: {
type: 'boolean',
required: false,
},
pages_to_collect: {
type: 'array',
required: false,
items: {
type: 'string'
}
}
}
}
app.put(constants.API_BASE_PATH + 'settings', validate({ body: UpdateConfigSchema }), async (req, res) => {
2023-01-17 04:35:16 +00:00
const user = await Users.findOne({ where: { 'id': req.session.user_id } });
if(! user){
return res.send("Invalid");
}
2021-05-31 19:06:40 +00:00
if(req.body.correlation_api_key === true) {
2023-01-17 04:35:16 +00:00
user.injectionCorrelationAPIKey = req.body.correlation_api_key;
2021-05-31 19:06:40 +00:00
}
// Intentionally no URL validation incase people want to do
// data: for inline extra JS.
if(req.body.chainload_uri) {
2023-01-17 04:35:16 +00:00
user.additionalJS = req.body.chainload_uri;
2021-05-31 19:06:40 +00:00
}
if(req.body.send_alert_emails !== undefined) {
2023-01-17 04:35:16 +00:00
user.sendEmailAlerts = req.body.send_alert_emails;
2021-05-31 19:06:40 +00:00
}
2023-01-17 04:35:16 +00:00
await user.save();
2021-05-31 19:06:40 +00:00
res.status(200).json({
'success': true,
'result': {}
}).end();
});
}
module.exports = {
set_up_api_server
2023-01-16 01:52:10 +00:00
};