Skip to content

Commit 258c0a0

Browse files
rubentalstraandresgitmawburngithub-advanced-security[bot]
authored andcommitted
🔒 feat: Add Content Security Policy using Helmet middleware (danny-avila#7377)
* 🔒 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 (danny-avila#7308) When selecting equations and using copy paste, uses the correct latex code. Co-authored-by: Ruben Talstra <[email protected]> * 🔃 refactor: `AgentFooter` to conditionally render buttons based on `activePanel` (danny-avila#7306) * 🚀 feat: Add `Cloudflare Turnstile` support (danny-avila#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 <[email protected]> Co-authored-by: matt burnett <[email protected]> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
1 parent 744d1d6 commit 258c0a0

File tree

6 files changed

+93
-60
lines changed

6 files changed

+93
-60
lines changed

api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"firebase": "^11.0.2",
7272
"googleapis": "^126.0.1",
7373
"handlebars": "^4.7.7",
74+
"helmet": "^8.1.0",
7475
"https-proxy-agent": "^7.0.6",
7576
"ioredis": "^5.3.2",
7677
"js-yaml": "^4.1.0",

api/server/index.js

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ require('dotenv').config();
22
const path = require('path');
33
require('module-alias')({ base: path.resolve(__dirname, '..') });
44
const cors = require('cors');
5+
const helmet = require('helmet');
56
const axios = require('axios');
67
const express = require('express');
78
const compression = require('compression');
@@ -22,7 +23,15 @@ const staticCache = require('./utils/staticCache');
2223
const noIndex = require('./middleware/noIndex');
2324
const routes = require('./routes');
2425

25-
const { PORT, HOST, ALLOW_SOCIAL_LOGIN, DISABLE_COMPRESSION, TRUST_PROXY } = process.env ?? {};
26+
const {
27+
PORT,
28+
HOST,
29+
ALLOW_SOCIAL_LOGIN,
30+
DISABLE_COMPRESSION,
31+
TRUST_PROXY,
32+
SANDPACK_BUNDLER_URL,
33+
SANDPACK_STATIC_BUNDLER_URL,
34+
} = process.env ?? {};
2635

2736
const port = Number(PORT) || 3080;
2837
const host = HOST || 'localhost';
@@ -38,6 +47,8 @@ const startServer = async () => {
3847

3948
const app = express();
4049
app.disable('x-powered-by');
50+
app.set('trust proxy', trusted_proxy);
51+
4152
await AppService(app);
4253

4354
const indexPath = path.join(app.locals.paths.dist, 'index.html');
@@ -49,23 +60,54 @@ const startServer = async () => {
4960
app.use(noIndex);
5061
app.use(errorController);
5162
app.use(express.json({ limit: '3mb' }));
52-
app.use(mongoSanitize());
5363
app.use(express.urlencoded({ extended: true, limit: '3mb' }));
54-
app.use(staticCache(app.locals.paths.dist));
55-
app.use(staticCache(app.locals.paths.fonts));
56-
app.use(staticCache(app.locals.paths.assets));
57-
app.set('trust proxy', trusted_proxy);
64+
app.use(mongoSanitize());
5865
app.use(cors());
5966
app.use(cookieParser());
67+
app.use(
68+
helmet({
69+
contentSecurityPolicy: {
70+
useDefaults: false,
71+
directives: {
72+
defaultSrc: ["'self'"],
73+
scriptSrc: ["'self'", "'unsafe-inline'", 'https://challenges.cloudflare.com'],
74+
styleSrc: ["'self'", "'unsafe-inline'"],
75+
fontSrc: ["'self'", 'data:'],
76+
objectSrc: ["'none'"],
77+
imgSrc: ["'self'", 'data:'],
78+
mediaSrc: ["'self'", 'data:', 'blob:'],
79+
connectSrc: ["'self'"],
80+
frameSrc: [
81+
"'self'",
82+
'https://challenges.cloudflare.com',
83+
'https://codesandbox.io',
84+
...(SANDPACK_BUNDLER_URL ? [SANDPACK_BUNDLER_URL] : []),
85+
...(SANDPACK_STATIC_BUNDLER_URL ? [SANDPACK_STATIC_BUNDLER_URL] : []),
86+
],
87+
frameAncestors: [
88+
"'self'",
89+
'https://codesandbox.io',
90+
...(SANDPACK_BUNDLER_URL ? [SANDPACK_BUNDLER_URL] : []),
91+
...(SANDPACK_STATIC_BUNDLER_URL ? [SANDPACK_STATIC_BUNDLER_URL] : []),
92+
],
93+
},
94+
},
95+
}),
96+
);
6097

6198
if (!isEnabled(DISABLE_COMPRESSION)) {
6299
app.use(compression());
100+
} else {
101+
console.warn('Response compression has been disabled via DISABLE_COMPRESSION.');
63102
}
64103

104+
// Serve static assets with aggressive caching
105+
app.use(staticCache(app.locals.paths.dist));
106+
app.use(staticCache(app.locals.paths.fonts));
107+
app.use(staticCache(app.locals.paths.assets));
108+
65109
if (!ALLOW_SOCIAL_LOGIN) {
66-
console.warn(
67-
'Social logins are disabled. Set Environment Variable "ALLOW_SOCIAL_LOGIN" to true to enable them.',
68-
);
110+
console.warn('Social logins are disabled. Set ALLOW_SOCIAL_LOGIN=true to enable them.');
69111
}
70112

71113
/* OAUTH */
@@ -128,7 +170,7 @@ const startServer = async () => {
128170
});
129171

130172
app.listen(port, host, () => {
131-
if (host == '0.0.0.0') {
173+
if (host === '0.0.0.0') {
132174
logger.info(
133175
`Server listening on all interfaces at port ${port}. Use http://localhost:${port} to access it`,
134176
);

api/server/services/start/turnstile.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,16 @@ function loadTurnstileConfig(config, configDefaults) {
2626
options: customTurnstile.options ?? defaults.options,
2727
});
2828

29-
logger.info('Turnstile configuration loaded:\n' + JSON.stringify(loadedTurnstile, null, 2));
29+
const enabled = Boolean(loadedTurnstile.siteKey);
30+
31+
if (enabled) {
32+
logger.info(
33+
'Turnstile is ENABLED with configuration:\n' + JSON.stringify(loadedTurnstile, null, 2),
34+
);
35+
} else {
36+
logger.info('Turnstile is DISABLED (no siteKey provided).');
37+
}
38+
3039
return loadedTurnstile;
3140
}
3241

client/src/components/Auth/LoginForm.tsx

Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type TLoginFormProps = {
1616
const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error, setError }) => {
1717
const localize = useLocalize();
1818
const { theme } = useContext(ThemeContext);
19+
1920
const {
2021
register,
2122
getValues,
@@ -28,6 +29,7 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
2829
const { data: config } = useGetStartupConfig();
2930
const useUsernameLogin = config?.ldap?.username;
3031
const validTheme = theme === 'dark' ? 'dark' : 'light';
32+
const requireCaptcha = Boolean(startupConfig.turnstile?.siteKey);
3133

3234
useEffect(() => {
3335
if (error && error.includes('422') && !showResendLink) {
@@ -100,20 +102,12 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
100102
},
101103
})}
102104
aria-invalid={!!errors.email}
103-
className="
104-
webkit-dark-styles transition-color peer w-full rounded-2xl border border-border-light
105-
bg-surface-primary px-3.5 pb-2.5 pt-3 text-text-primary duration-200 focus:border-green-500 focus:outline-none
106-
"
105+
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"
107106
placeholder=" "
108107
/>
109108
<label
110109
htmlFor="email"
111-
className="
112-
absolute start-3 top-1.5 z-10 origin-[0] -translate-y-4 scale-75 transform bg-surface-primary px-2 text-sm text-text-secondary-alt duration-200
113-
peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100
114-
peer-focus:top-1.5 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 peer-focus:text-green-600 dark:peer-focus:text-green-500
115-
rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4
116-
"
110+
className="absolute start-3 top-1.5 z-10 origin-[0] -translate-y-4 scale-75 transform bg-surface-primary px-2 text-sm text-text-secondary-alt duration-200 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-1.5 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 peer-focus:text-green-600 dark:peer-focus:text-green-500"
117111
>
118112
{useUsernameLogin
119113
? localize('com_auth_username').replace(/ \(.*$/, '')
@@ -135,20 +129,12 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
135129
maxLength: { value: 128, message: localize('com_auth_password_max_length') },
136130
})}
137131
aria-invalid={!!errors.password}
138-
className="
139-
webkit-dark-styles transition-color peer w-full rounded-2xl border border-border-light
140-
bg-surface-primary px-3.5 pb-2.5 pt-3 text-text-primary duration-200 focus:border-green-500 focus:outline-none
141-
"
132+
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"
142133
placeholder=" "
143134
/>
144135
<label
145136
htmlFor="password"
146-
className="
147-
absolute start-3 top-1.5 z-10 origin-[0] -translate-y-4 scale-75 transform bg-surface-primary px-2 text-sm text-text-secondary-alt duration-200
148-
peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100
149-
peer-focus:top-1.5 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 peer-focus:text-green-600 dark:peer-focus:text-green-500
150-
rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4
151-
"
137+
className="absolute start-3 top-1.5 z-10 origin-[0] -translate-y-4 scale-75 transform bg-surface-primary px-2 text-sm text-text-secondary-alt duration-200 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-1.5 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 peer-focus:text-green-600 dark:peer-focus:text-green-500"
152138
>
153139
{localize('com_auth_password')}
154140
</label>
@@ -164,16 +150,15 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
164150
</a>
165151
)}
166152

167-
{/* Render Turnstile only if enabled in startupConfig */}
168-
{startupConfig.turnstile && (
153+
{requireCaptcha && (
169154
<div className="my-4 flex justify-center">
170155
<Turnstile
171-
siteKey={startupConfig.turnstile.siteKey}
156+
siteKey={startupConfig.turnstile!.siteKey}
172157
options={{
173-
...startupConfig.turnstile.options,
158+
...startupConfig.turnstile!.options,
174159
theme: validTheme,
175160
}}
176-
onSuccess={(token) => setTurnstileToken(token)}
161+
onSuccess={setTurnstileToken}
177162
onError={() => setTurnstileToken(null)}
178163
onExpire={() => setTurnstileToken(null)}
179164
/>
@@ -185,11 +170,8 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
185170
aria-label={localize('com_auth_continue')}
186171
data-testid="login-button"
187172
type="submit"
188-
disabled={startupConfig.turnstile ? !turnstileToken : false}
189-
className="
190-
w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white
191-
transition-colors hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-700
192-
"
173+
disabled={requireCaptcha && !turnstileToken}
174+
className="w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white transition-colors hover:bg-green-700 disabled:opacity-50 disabled:hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700"
193175
>
194176
{localize('com_auth_continue')}
195177
</button>

client/src/components/Auth/Registration.tsx

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ const Registration: React.FC = () => {
3333
const token = queryParams.get('token');
3434
const validTheme = theme === 'dark' ? 'dark' : 'light';
3535

36+
// only require captcha if we have a siteKey
37+
const requireCaptcha = Boolean(startupConfig?.turnstile?.siteKey);
38+
3639
const registerUser = useRegisterUserMutation({
3740
onMutate: () => {
3841
setIsSubmitting(true);
@@ -73,21 +76,13 @@ const Registration: React.FC = () => {
7376
validation,
7477
)}
7578
aria-invalid={!!errors[id]}
76-
className="
77-
webkit-dark-styles transition-color peer w-full rounded-2xl border border-border-light
78-
bg-surface-primary px-3.5 pb-2.5 pt-3 text-text-primary duration-200 focus:border-green-500 focus:outline-none
79-
"
79+
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"
8080
placeholder=" "
8181
data-testid={id}
8282
/>
8383
<label
8484
htmlFor={id}
85-
className="
86-
absolute start-3 top-1.5 z-10 origin-[0] -translate-y-4 scale-75 transform bg-surface-primary px-2 text-sm text-text-secondary-alt duration-200
87-
peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100
88-
peer-focus:top-1.5 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 peer-focus:text-green-500
89-
rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4
90-
"
85+
className="absolute start-3 top-1.5 z-10 origin-[0] -translate-y-4 scale-75 transform bg-surface-primary px-2 text-sm text-text-secondary-alt duration-200 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-1.5 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 peer-focus:text-green-500 rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4"
9186
>
9287
{localize(label)}
9388
</label>
@@ -183,8 +178,7 @@ const Registration: React.FC = () => {
183178
value === password || localize('com_auth_password_not_match'),
184179
})}
185180

186-
{/* Render Turnstile only if enabled in startupConfig */}
187-
{startupConfig?.turnstile && (
181+
{startupConfig?.turnstile?.siteKey && (
188182
<div className="my-4 flex justify-center">
189183
<Turnstile
190184
siteKey={startupConfig.turnstile.siteKey}
@@ -204,16 +198,11 @@ const Registration: React.FC = () => {
204198
disabled={
205199
Object.keys(errors).length > 0 ||
206200
isSubmitting ||
207-
(startupConfig?.turnstile ? !turnstileToken : false)
201+
(requireCaptcha && !turnstileToken)
208202
}
209203
type="submit"
210204
aria-label="Submit registration"
211-
className="
212-
w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white
213-
transition-colors hover:bg-green-700 focus:outline-none focus:ring-2
214-
focus:ring-green-500 focus:ring-offset-2 disabled:opacity-50
215-
disabled:hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700
216-
"
205+
className="w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white transition-colors hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 disabled:opacity-50 disabled:hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700"
217206
>
218207
{isSubmitting ? <Spinner /> : localize('com_auth_continue')}
219208
</button>

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)