From 7a91f6ca62865b0e6b94b816e59f0f39382a9832 Mon Sep 17 00:00:00 2001 From: Ruben Talstra Date: Thu, 15 May 2025 22:25:10 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=20feat:=20Add=20Content=20Security?= =?UTF-8?q?=20Policy=20using=20Helmet=20middleware=20(#7377)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔒 feat: Add Content Security Policy using Helmet middleware * 🔒 feat: Set trust proxy and refine Content Security Policy directives * 🎨 feat: add `copy-tex` to improve copying KaTeX (#7308) When selecting equations and using copy paste, uses the correct latex code. Co-authored-by: Ruben Talstra * 🔃 refactor: `AgentFooter` to conditionally render buttons based on `activePanel` (#7306) * 🚀 feat: Add `Cloudflare Turnstile` support (#5987) * 🚀 feat: Add @marsidev/react-turnstile dependency to package.json and package-lock.json * 🚀 feat: Integrate Cloudflare Turnstile configuration support in AppService and add schema validation * 🚀 feat: Implemented Cloudflare Turnstile integration in Login and Registration forms * 🚀 feat: Enhance AppService tests with additional mocks and configuration setups * 🚀 feat: Comment out outdated config version warning tests in AppService.spec.js * 🚀 feat: Remove outdated warning tests and add new checks for environment variables and API health * 🔧 test: Update AppService.spec.js to use expect.anything() for paths validation * 🔧 test: Refactor AppService.spec.js to streamline mocks and enhance clarity * 🔧 chore: removed not needed test * Potential fix for code scanning alert no. 5638: Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for code scanning alert no. 5629: Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for code scanning alert no. 5642: Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Update turnstile.js * Potential fix for code scanning alert no. 5634: Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for code scanning alert no. 5646: Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for code scanning alert no. 5647: Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * 🔒 feat: Refactor Content Security Policy setup to use Helmet middleware with custom directives * 🔒 feat: Enhance Content Security Policy to include Sandpack Bundler URL * 🔒 feat: Update Content Security Policy and integrate Turnstile captcha support --------- Co-authored-by: andresgit <9771158+andresgit@users.noreply.github.com> Co-authored-by: matt burnett Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- api/package.json | 1 + api/server/index.js | 62 +++++++++++++++++---- api/server/services/start/turnstile.js | 11 +++- client/src/components/Auth/LoginForm.tsx | 42 ++++---------- client/src/components/Auth/Registration.tsx | 27 +++------ package-lock.json | 10 ++++ 6 files changed, 93 insertions(+), 60 deletions(-) diff --git a/api/package.json b/api/package.json index bcf94a6cad..8782457dbc 100644 --- a/api/package.json +++ b/api/package.json @@ -71,6 +71,7 @@ "firebase": "^11.0.2", "googleapis": "^126.0.1", "handlebars": "^4.7.7", + "helmet": "^8.1.0", "https-proxy-agent": "^7.0.6", "ioredis": "^5.3.2", "js-yaml": "^4.1.0", diff --git a/api/server/index.js b/api/server/index.js index cd0bdd3f88..3c8d3dd951 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -2,6 +2,7 @@ require('dotenv').config(); const path = require('path'); require('module-alias')({ base: path.resolve(__dirname, '..') }); const cors = require('cors'); +const helmet = require('helmet'); const axios = require('axios'); const express = require('express'); const compression = require('compression'); @@ -22,7 +23,15 @@ const staticCache = require('./utils/staticCache'); const noIndex = require('./middleware/noIndex'); const routes = require('./routes'); -const { PORT, HOST, ALLOW_SOCIAL_LOGIN, DISABLE_COMPRESSION, TRUST_PROXY } = process.env ?? {}; +const { + PORT, + HOST, + ALLOW_SOCIAL_LOGIN, + DISABLE_COMPRESSION, + TRUST_PROXY, + SANDPACK_BUNDLER_URL, + SANDPACK_STATIC_BUNDLER_URL, +} = process.env ?? {}; const port = Number(PORT) || 3080; const host = HOST || 'localhost'; @@ -38,6 +47,8 @@ const startServer = async () => { const app = express(); app.disable('x-powered-by'); + app.set('trust proxy', trusted_proxy); + await AppService(app); const indexPath = path.join(app.locals.paths.dist, 'index.html'); @@ -49,23 +60,54 @@ const startServer = async () => { app.use(noIndex); app.use(errorController); app.use(express.json({ limit: '3mb' })); - app.use(mongoSanitize()); app.use(express.urlencoded({ extended: true, limit: '3mb' })); - app.use(staticCache(app.locals.paths.dist)); - app.use(staticCache(app.locals.paths.fonts)); - app.use(staticCache(app.locals.paths.assets)); - app.set('trust proxy', trusted_proxy); + app.use(mongoSanitize()); app.use(cors()); app.use(cookieParser()); + app.use( + helmet({ + contentSecurityPolicy: { + useDefaults: false, + directives: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'", "'unsafe-inline'", 'https://challenges.cloudflare.com'], + styleSrc: ["'self'", "'unsafe-inline'"], + fontSrc: ["'self'", 'data:'], + objectSrc: ["'none'"], + imgSrc: ["'self'", 'data:'], + mediaSrc: ["'self'", 'data:', 'blob:'], + connectSrc: ["'self'"], + frameSrc: [ + "'self'", + 'https://challenges.cloudflare.com', + 'https://codesandbox.io', + ...(SANDPACK_BUNDLER_URL ? [SANDPACK_BUNDLER_URL] : []), + ...(SANDPACK_STATIC_BUNDLER_URL ? [SANDPACK_STATIC_BUNDLER_URL] : []), + ], + frameAncestors: [ + "'self'", + 'https://codesandbox.io', + ...(SANDPACK_BUNDLER_URL ? [SANDPACK_BUNDLER_URL] : []), + ...(SANDPACK_STATIC_BUNDLER_URL ? [SANDPACK_STATIC_BUNDLER_URL] : []), + ], + }, + }, + }), + ); if (!isEnabled(DISABLE_COMPRESSION)) { app.use(compression()); + } else { + console.warn('Response compression has been disabled via DISABLE_COMPRESSION.'); } + // Serve static assets with aggressive caching + app.use(staticCache(app.locals.paths.dist)); + app.use(staticCache(app.locals.paths.fonts)); + app.use(staticCache(app.locals.paths.assets)); + if (!ALLOW_SOCIAL_LOGIN) { - console.warn( - 'Social logins are disabled. Set Environment Variable "ALLOW_SOCIAL_LOGIN" to true to enable them.', - ); + console.warn('Social logins are disabled. Set ALLOW_SOCIAL_LOGIN=true to enable them.'); } /* OAUTH */ @@ -128,7 +170,7 @@ const startServer = async () => { }); app.listen(port, host, () => { - if (host == '0.0.0.0') { + if (host === '0.0.0.0') { logger.info( `Server listening on all interfaces at port ${port}. Use http://localhost:${port} to access it`, ); diff --git a/api/server/services/start/turnstile.js b/api/server/services/start/turnstile.js index ffd4545dae..be9e5f83c7 100644 --- a/api/server/services/start/turnstile.js +++ b/api/server/services/start/turnstile.js @@ -26,7 +26,16 @@ function loadTurnstileConfig(config, configDefaults) { options: customTurnstile.options ?? defaults.options, }); - logger.info('Turnstile configuration loaded:\n' + JSON.stringify(loadedTurnstile, null, 2)); + const enabled = Boolean(loadedTurnstile.siteKey); + + if (enabled) { + logger.info( + 'Turnstile is ENABLED with configuration:\n' + JSON.stringify(loadedTurnstile, null, 2), + ); + } else { + logger.info('Turnstile is DISABLED (no siteKey provided).'); + } + return loadedTurnstile; } diff --git a/client/src/components/Auth/LoginForm.tsx b/client/src/components/Auth/LoginForm.tsx index 030b6323f7..e34ec3c94d 100644 --- a/client/src/components/Auth/LoginForm.tsx +++ b/client/src/components/Auth/LoginForm.tsx @@ -16,6 +16,7 @@ type TLoginFormProps = { const LoginForm: React.FC = ({ onSubmit, startupConfig, error, setError }) => { const localize = useLocalize(); const { theme } = useContext(ThemeContext); + const { register, getValues, @@ -28,6 +29,7 @@ const LoginForm: React.FC = ({ onSubmit, startupConfig, error, const { data: config } = useGetStartupConfig(); const useUsernameLogin = config?.ldap?.username; const validTheme = theme === 'dark' ? 'dark' : 'light'; + const requireCaptcha = Boolean(startupConfig.turnstile?.siteKey); useEffect(() => { if (error && error.includes('422') && !showResendLink) { @@ -100,20 +102,12 @@ const LoginForm: React.FC = ({ onSubmit, startupConfig, error, }, })} aria-invalid={!!errors.email} - className=" - webkit-dark-styles transition-color peer w-full rounded-2xl border border-border-light - bg-surface-primary px-3.5 pb-2.5 pt-3 text-text-primary duration-200 focus:border-green-500 focus:outline-none - " + className="peer w-full rounded-2xl border border-border-light bg-surface-primary px-3.5 pb-2.5 pt-3 text-text-primary transition-colors duration-200 focus:border-green-500 focus:outline-none" placeholder=" " />