mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Move security checks to users.js
This commit is contained in:
6
index.d.ts
vendored
6
index.d.ts
vendored
@@ -1,4 +1,5 @@
|
|||||||
import { UserDirectoryList, User } from "./src/users";
|
import { UserDirectoryList, User } from "./src/users";
|
||||||
|
import { CommandLineArguments } from "./src/command-line";
|
||||||
import { CsrfSyncedToken } from "csrf-sync";
|
import { CsrfSyncedToken } from "csrf-sync";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -32,4 +33,9 @@ declare global {
|
|||||||
* The root directory for user data.
|
* The root directory for user data.
|
||||||
*/
|
*/
|
||||||
var DATA_ROOT: string;
|
var DATA_ROOT: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parsed command line arguments.
|
||||||
|
*/
|
||||||
|
var COMMAND_LINE_ARGS: CommandLineArguments;
|
||||||
}
|
}
|
||||||
|
138
server.js
138
server.js
@@ -26,7 +26,6 @@ import {
|
|||||||
initUserStorage,
|
initUserStorage,
|
||||||
getCookieSecret,
|
getCookieSecret,
|
||||||
getCookieSessionName,
|
getCookieSessionName,
|
||||||
getAllEnabledUsers,
|
|
||||||
ensurePublicDirectoriesExist,
|
ensurePublicDirectoriesExist,
|
||||||
getUserDirectoriesList,
|
getUserDirectoriesList,
|
||||||
migrateSystemPrompts,
|
migrateSystemPrompts,
|
||||||
@@ -34,9 +33,10 @@ import {
|
|||||||
requireLoginMiddleware,
|
requireLoginMiddleware,
|
||||||
setUserDataMiddleware,
|
setUserDataMiddleware,
|
||||||
shouldRedirectToLogin,
|
shouldRedirectToLogin,
|
||||||
tryAutoLogin,
|
|
||||||
cleanUploads,
|
cleanUploads,
|
||||||
getSessionCookieAge,
|
getSessionCookieAge,
|
||||||
|
verifySecuritySettings,
|
||||||
|
loginPageMiddleware,
|
||||||
} from './src/users.js';
|
} from './src/users.js';
|
||||||
|
|
||||||
import getWebpackServeMiddleware from './src/middleware/webpack-serve.js';
|
import getWebpackServeMiddleware from './src/middleware/webpack-serve.js';
|
||||||
@@ -49,7 +49,6 @@ import getCacheBusterMiddleware from './src/middleware/cacheBuster.js';
|
|||||||
import corsProxyMiddleware from './src/middleware/corsProxy.js';
|
import corsProxyMiddleware from './src/middleware/corsProxy.js';
|
||||||
import {
|
import {
|
||||||
getVersion,
|
getVersion,
|
||||||
getConfigValue,
|
|
||||||
color,
|
color,
|
||||||
removeColorFormatting,
|
removeColorFormatting,
|
||||||
getSeparator,
|
getSeparator,
|
||||||
@@ -72,6 +71,11 @@ util.inspect.defaultOptions.maxArrayLength = null;
|
|||||||
util.inspect.defaultOptions.maxStringLength = null;
|
util.inspect.defaultOptions.maxStringLength = null;
|
||||||
util.inspect.defaultOptions.depth = 4;
|
util.inspect.defaultOptions.depth = 4;
|
||||||
|
|
||||||
|
// Set a working directory for the server
|
||||||
|
const serverDirectory = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
console.log(`Node version: ${process.version}. Running in ${process.env.NODE_ENV} environment. Server directory: ${serverDirectory}`);
|
||||||
|
process.chdir(serverDirectory);
|
||||||
|
|
||||||
// Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0.
|
// Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0.
|
||||||
// https://github.com/nodejs/node/issues/47822#issuecomment-1564708870
|
// https://github.com/nodejs/node/issues/47822#issuecomment-1564708870
|
||||||
// Safe to remove once support for Node v20 is dropped.
|
// Safe to remove once support for Node v20 is dropped.
|
||||||
@@ -80,19 +84,26 @@ if (process.versions && process.versions.node && process.versions.node.match(/20
|
|||||||
if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false);
|
if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cliParser = new CommandLineParser();
|
const cliArgs = new CommandLineParser().parse(process.argv);
|
||||||
const cliArgs = cliParser.parse(process.argv);
|
|
||||||
globalThis.DATA_ROOT = cliArgs.dataRoot;
|
globalThis.DATA_ROOT = cliArgs.dataRoot;
|
||||||
|
globalThis.COMMAND_LINE_ARGS = cliArgs;
|
||||||
|
|
||||||
if (!cliArgs.enableIPv6 && !cliArgs.enableIPv4) {
|
if (!cliArgs.enableIPv6 && !cliArgs.enableIPv4) {
|
||||||
console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.');
|
console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// change all relative paths
|
try {
|
||||||
const serverDirectory = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url));
|
if (cliArgs.dnsPreferIPv6) {
|
||||||
console.log(`Node version: ${process.version}. Running in ${process.env.NODE_ENV} environment. Server directory: ${serverDirectory}`);
|
dns.setDefaultResultOrder('ipv6first');
|
||||||
process.chdir(serverDirectory);
|
console.log('Preferring IPv6 for DNS resolution');
|
||||||
|
} else {
|
||||||
|
dns.setDefaultResultOrder('ipv4first');
|
||||||
|
console.log('Preferring IPv4 for DNS resolution');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to set DNS resolution order. Possibly unsupported in this Node version.');
|
||||||
|
}
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(helmet({
|
app.use(helmet({
|
||||||
@@ -101,19 +112,6 @@ app.use(helmet({
|
|||||||
app.use(compression());
|
app.use(compression());
|
||||||
app.use(responseTime());
|
app.use(responseTime());
|
||||||
|
|
||||||
/** @type {boolean} */
|
|
||||||
const enableAccounts = getConfigValue('enableUserAccounts', false, 'boolean');
|
|
||||||
|
|
||||||
if (cliArgs.dnsPreferIPv6) {
|
|
||||||
// Set default DNS resolution order to IPv6 first
|
|
||||||
dns.setDefaultResultOrder('ipv6first');
|
|
||||||
console.log('Preferring IPv6 for DNS resolution');
|
|
||||||
} else {
|
|
||||||
// Set default DNS resolution order to IPv4 first
|
|
||||||
dns.setDefaultResultOrder('ipv4first');
|
|
||||||
console.log('Preferring IPv4 for DNS resolution');
|
|
||||||
}
|
|
||||||
|
|
||||||
// CORS Settings //
|
// CORS Settings //
|
||||||
const CORS = cors({
|
const CORS = cors({
|
||||||
origin: 'null',
|
origin: 'null',
|
||||||
@@ -213,24 +211,7 @@ app.get('/', getCacheBusterMiddleware(), (request, response) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Host login page
|
// Host login page
|
||||||
app.get('/login', async (request, response) => {
|
app.get('/login', loginPageMiddleware);
|
||||||
if (!enableAccounts) {
|
|
||||||
console.log('User accounts are disabled. Redirecting to index page.');
|
|
||||||
return response.redirect('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const autoLogin = await tryAutoLogin(request, cliArgs.basicAuthMode);
|
|
||||||
|
|
||||||
if (autoLogin) {
|
|
||||||
return response.redirect('/');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error during auto-login:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.sendFile('login.html', { root: path.join(process.cwd(), 'public') });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Host frontend assets
|
// Host frontend assets
|
||||||
const webpackMiddleware = getWebpackServeMiddleware();
|
const webpackMiddleware = getWebpackServeMiddleware();
|
||||||
@@ -289,7 +270,8 @@ async function preSetupTasks() {
|
|||||||
await settingsInit();
|
await settingsInit();
|
||||||
await statsInit();
|
await statsInit();
|
||||||
|
|
||||||
const cleanupPlugins = await initializePlugins();
|
const pluginsDirectory = path.join(serverDirectory, 'plugins');
|
||||||
|
const cleanupPlugins = await loadPlugins(app, pluginsDirectory);
|
||||||
const consoleTitle = process.title;
|
const consoleTitle = process.title;
|
||||||
|
|
||||||
let isExiting = false;
|
let isExiting = false;
|
||||||
@@ -365,80 +347,6 @@ async function postSetupTasks(result) {
|
|||||||
setupLogLevel();
|
setupLogLevel();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads server plugins from a directory.
|
|
||||||
* @returns {Promise<Function>} Function to be run on server exit
|
|
||||||
*/
|
|
||||||
async function initializePlugins() {
|
|
||||||
try {
|
|
||||||
const pluginDirectory = path.join(serverDirectory, 'plugins');
|
|
||||||
const cleanupPlugins = await loadPlugins(app, pluginDirectory);
|
|
||||||
return cleanupPlugins;
|
|
||||||
} catch {
|
|
||||||
console.log('Plugin loading failed.');
|
|
||||||
return () => { };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prints an error message and exits the process if necessary
|
|
||||||
* @param {string} message The error message to print
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
function logSecurityAlert(message) {
|
|
||||||
if (cliArgs.basicAuthMode || cliArgs.whitelistMode) return; // safe!
|
|
||||||
console.error(color.red(message));
|
|
||||||
if (getConfigValue('securityOverride', false, 'boolean')) {
|
|
||||||
console.warn(color.red('Security has been overridden. If it\'s not a trusted network, change the settings.'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function verifySecuritySettings() {
|
|
||||||
// Skip all security checks as listen is set to false
|
|
||||||
if (!cliArgs.listen) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!enableAccounts) {
|
|
||||||
logSecurityAlert('Your current SillyTavern configuration is insecure (listening to non-localhost). Enable whitelisting, basic authentication or user accounts.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const users = await getAllEnabledUsers();
|
|
||||||
const unprotectedUsers = users.filter(x => !x.password);
|
|
||||||
const unprotectedAdminUsers = unprotectedUsers.filter(x => x.admin);
|
|
||||||
|
|
||||||
if (unprotectedUsers.length > 0) {
|
|
||||||
console.warn(color.blue('A friendly reminder that the following users are not password protected:'));
|
|
||||||
unprotectedUsers.map(x => `${color.yellow(x.handle)} ${color.red(x.admin ? '(admin)' : '')}`).forEach(x => console.warn(x));
|
|
||||||
console.log();
|
|
||||||
console.warn(`Consider setting a password in the admin panel or by using the ${color.blue('recover.js')} script.`);
|
|
||||||
console.log();
|
|
||||||
|
|
||||||
if (unprotectedAdminUsers.length > 0) {
|
|
||||||
logSecurityAlert('If you are not using basic authentication or whitelisting, you should set a password for all admin users.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cliArgs.basicAuthMode) {
|
|
||||||
const perUserBasicAuth = getConfigValue('perUserBasicAuth', false, 'boolean');
|
|
||||||
if (perUserBasicAuth && !enableAccounts) {
|
|
||||||
console.error(color.red(
|
|
||||||
'Per-user basic authentication is enabled, but user accounts are disabled. This configuration may be insecure.',
|
|
||||||
));
|
|
||||||
} else if (!perUserBasicAuth) {
|
|
||||||
const basicAuthUserName = getConfigValue('basicAuthUser.username', '');
|
|
||||||
const basicAuthUserPassword = getConfigValue('basicAuthUser.password', '');
|
|
||||||
if (!basicAuthUserName || !basicAuthUserPassword) {
|
|
||||||
console.warn(color.yellow(
|
|
||||||
'Basic Authentication is enabled, but username or password is not set or empty!',
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers a not-found error response if a not-found error page exists. Should only be called after all other middlewares have been registered.
|
* Registers a not-found error response if a not-found error page exists. Should only be called after all other middlewares have been registered.
|
||||||
*/
|
*/
|
||||||
|
@@ -38,6 +38,7 @@ const isESModule = (file) => path.extname(file) === '.mjs';
|
|||||||
* be called before the server shuts down.
|
* be called before the server shuts down.
|
||||||
*/
|
*/
|
||||||
export async function loadPlugins(app, pluginsPath) {
|
export async function loadPlugins(app, pluginsPath) {
|
||||||
|
try {
|
||||||
const exitHooks = [];
|
const exitHooks = [];
|
||||||
const emptyFn = () => { };
|
const emptyFn = () => { };
|
||||||
|
|
||||||
@@ -82,6 +83,10 @@ export async function loadPlugins(app, pluginsPath) {
|
|||||||
|
|
||||||
// Call all plugin "exit" functions at once and wait for them to finish
|
// Call all plugin "exit" functions at once and wait for them to finish
|
||||||
return () => Promise.all(exitHooks.map(exitFn => exitFn()));
|
return () => Promise.all(exitHooks.map(exitFn => exitFn()));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Plugin loading failed.', error);
|
||||||
|
return () => { };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadFromDirectory(app, pluginDirectoryPath, exitHooks) {
|
async function loadFromDirectory(app, pluginDirectoryPath, exitHooks) {
|
||||||
|
91
src/users.js
91
src/users.js
@@ -120,6 +120,72 @@ export async function ensurePublicDirectoriesExist() {
|
|||||||
return directoriesList;
|
return directoriesList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prints an error message and exits the process if necessary
|
||||||
|
* @param {string} message The error message to print
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function logSecurityAlert(message) {
|
||||||
|
const { basicAuthMode, whitelistMode } = globalThis.COMMAND_LINE_ARGS;
|
||||||
|
if (basicAuthMode || whitelistMode) return; // safe!
|
||||||
|
console.error(color.red(message));
|
||||||
|
if (getConfigValue('securityOverride', false, 'boolean')) {
|
||||||
|
console.warn(color.red('Security has been overridden. If it\'s not a trusted network, change the settings.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the security settings and prints warnings if necessary
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export async function verifySecuritySettings() {
|
||||||
|
const { listen, basicAuthMode } = globalThis.COMMAND_LINE_ARGS;
|
||||||
|
|
||||||
|
// Skip all security checks as listen is set to false
|
||||||
|
if (!listen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ENABLE_ACCOUNTS) {
|
||||||
|
logSecurityAlert('Your current SillyTavern configuration is insecure (listening to non-localhost). Enable whitelisting, basic authentication or user accounts.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = await getAllEnabledUsers();
|
||||||
|
const unprotectedUsers = users.filter(x => !x.password);
|
||||||
|
const unprotectedAdminUsers = unprotectedUsers.filter(x => x.admin);
|
||||||
|
|
||||||
|
if (unprotectedUsers.length > 0) {
|
||||||
|
console.warn(color.blue('A friendly reminder that the following users are not password protected:'));
|
||||||
|
unprotectedUsers.map(x => `${color.yellow(x.handle)} ${color.red(x.admin ? '(admin)' : '')}`).forEach(x => console.warn(x));
|
||||||
|
console.log();
|
||||||
|
console.warn(`Consider setting a password in the admin panel or by using the ${color.blue('recover.js')} script.`);
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
if (unprotectedAdminUsers.length > 0) {
|
||||||
|
logSecurityAlert('If you are not using basic authentication or whitelisting, you should set a password for all admin users.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (basicAuthMode) {
|
||||||
|
const perUserBasicAuth = getConfigValue('perUserBasicAuth', false, 'boolean');
|
||||||
|
if (perUserBasicAuth && !ENABLE_ACCOUNTS) {
|
||||||
|
console.error(color.red(
|
||||||
|
'Per-user basic authentication is enabled, but user accounts are disabled. This configuration may be insecure.',
|
||||||
|
));
|
||||||
|
} else if (!perUserBasicAuth) {
|
||||||
|
const basicAuthUserName = getConfigValue('basicAuthUser.username', '');
|
||||||
|
const basicAuthUserPassword = getConfigValue('basicAuthUser.password', '');
|
||||||
|
if (!basicAuthUserName || !basicAuthUserPassword) {
|
||||||
|
console.warn(color.yellow(
|
||||||
|
'Basic Authentication is enabled, but username or password is not set or empty!',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function cleanUploads() {
|
export function cleanUploads() {
|
||||||
try {
|
try {
|
||||||
const uploadsPath = path.join(globalThis.DATA_ROOT, UPLOADS_DIRECTORY);
|
const uploadsPath = path.join(globalThis.DATA_ROOT, UPLOADS_DIRECTORY);
|
||||||
@@ -817,6 +883,31 @@ export function requireLoginMiddleware(request, response, next) {
|
|||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware to host the login page.
|
||||||
|
* @param {import('express').Request} request Request object
|
||||||
|
* @param {import('express').Response} response Response object
|
||||||
|
*/
|
||||||
|
export async function loginPageMiddleware(request, response) {
|
||||||
|
if (!ENABLE_ACCOUNTS) {
|
||||||
|
console.log('User accounts are disabled. Redirecting to index page.');
|
||||||
|
return response.redirect('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { basicAuthMode } = globalThis.COMMAND_LINE_ARGS;
|
||||||
|
const autoLogin = await tryAutoLogin(request, basicAuthMode);
|
||||||
|
|
||||||
|
if (autoLogin) {
|
||||||
|
return response.redirect('/');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during auto-login:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.sendFile('login.html', { root: path.join(process.cwd(), 'public') });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a route handler for serving files from a specific directory.
|
* Creates a route handler for serving files from a specific directory.
|
||||||
* @param {(req: import('express').Request) => string} directoryFn A function that returns the directory path to serve files from
|
* @param {(req: import('express').Request) => string} directoryFn A function that returns the directory path to serve files from
|
||||||
|
Reference in New Issue
Block a user