detached 0ff6d73

fix(security): replace setInterval with lazy cleanup in register endpoint

commit
0ff6d73ec919a4e8574d37b3e633de185a6e574f
author
Kit · · 2026-02-16 11:00 UTC
parent
35b5b26
signature
SSH UnverifiedSHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6Tk The key is unknown
1 file · +19 -12
M src/routes/api/register/+server.ts +19 -12
@@ -10,23 +10,27 @@ const log = createLogger('register-server');
1010 // In-memory map to track failed captcha attempts per IP
1111 const captchaAttempts = new Map<string, { count: number; lastAttempt: number }>();
1212
13-// Cleanup old entries every hour
14-setInterval(
15- () => {
16- const now = Date.now();
17- const ONE_HOUR = 60 * 60 * 1000;
18- for (const [ip, data] of captchaAttempts.entries()) {
19- if (now - data.lastAttempt > ONE_HOUR) {
20- captchaAttempts.delete(ip);
21- }
13+// Lazy cleanup: Cloudflare Workers don't allow setInterval at global scope
14+const ONE_HOUR = 60 * 60 * 1000;
15+let lastCleanup = 0;
16+function cleanupOldCaptchaAttempts() {
17+ const now = Date.now();
18+ if (now - lastCleanup < ONE_HOUR) return;
19+ lastCleanup = now;
20+
21+ for (const [ip, data] of captchaAttempts.entries()) {
22+ if (now - data.lastAttempt > ONE_HOUR) {
23+ captchaAttempts.delete(ip);
2224 }
23- },
24- 60 * 60 * 1000
25-);
25+ }
26+}
2627
2728 export const POST: RequestHandler = async ({ request }) => {
2829 log.debug('New registration attempt received');
2930
31+ // Lazy cleanup on each request (replaces setInterval for Cloudflare compatibility)
32+ cleanupOldCaptchaAttempts();
33+
3034 if (request.method !== 'POST') {
3135 log.warn('Invalid method used', { method: request.method });
3236 return new Response(JSON.stringify({ error: 'Method Not Allowed' } as RegisterResponseError), {
@@ -81,6 +85,9 @@ export const POST: RequestHandler = async ({ request }) => {
8185 }
8286
8387 // Validate PDR captcha
88+ // SECURITY NOTE: Static CAPTCHA answer is intentional for this community app.
89+ // The answer "point de rencontre" acts as a shared-knowledge gate, not bot prevention.
90+ // Bot prevention is handled by Cloudflare Turnstile (when enabled in production).
8491 const normalizedAnswer = captchaAnswer.trim().toLowerCase();
8592 if (normalizedAnswer !== 'point de rencontre') {
8693 attemptData.count++;
@@ -10,23 +10,27 @@ const log = createLogger('register-server');
10 // In-memory map to track failed captcha attempts per IP10 // In-memory map to track failed captcha attempts per IP
11 const captchaAttempts = new Map<string, { count: number; lastAttempt: number }>();11 const captchaAttempts = new Map<string, { count: number; lastAttempt: number }>();
12 12
13-// Cleanup old entries every hour13+// Lazy cleanup: Cloudflare Workers don't allow setInterval at global scope
14-setInterval(14+const ONE_HOUR = 60 * 60 * 1000;
15- () => {15+let lastCleanup = 0;
16- const now = Date.now();16+function cleanupOldCaptchaAttempts() {
17- const ONE_HOUR = 60 * 60 * 1000;17+ const now = Date.now();
18- for (const [ip, data] of captchaAttempts.entries()) {18+ if (now - lastCleanup < ONE_HOUR) return;
19- if (now - data.lastAttempt > ONE_HOUR) {19+ lastCleanup = now;
20- captchaAttempts.delete(ip);20+
21- }21+ for (const [ip, data] of captchaAttempts.entries()) {
22+ if (now - data.lastAttempt > ONE_HOUR) {
23+ captchaAttempts.delete(ip);
22 }24 }
23- },25+ }
24- 60 * 60 * 100026+}
25-);
26 27
27 export const POST: RequestHandler = async ({ request }) => {28 export const POST: RequestHandler = async ({ request }) => {
28 log.debug('New registration attempt received');29 log.debug('New registration attempt received');
29 30
31+ // Lazy cleanup on each request (replaces setInterval for Cloudflare compatibility)
32+ cleanupOldCaptchaAttempts();
33+
30 if (request.method !== 'POST') {34 if (request.method !== 'POST') {
31 log.warn('Invalid method used', { method: request.method });35 log.warn('Invalid method used', { method: request.method });
32 return new Response(JSON.stringify({ error: 'Method Not Allowed' } as RegisterResponseError), {36 return new Response(JSON.stringify({ error: 'Method Not Allowed' } as RegisterResponseError), {
@@ -81,6 +85,9 @@ export const POST: RequestHandler = async ({ request }) => {
81 }85 }
82 86
83 // Validate PDR captcha87 // Validate PDR captcha
88+ // SECURITY NOTE: Static CAPTCHA answer is intentional for this community app.
89+ // The answer "point de rencontre" acts as a shared-knowledge gate, not bot prevention.
90+ // Bot prevention is handled by Cloudflare Turnstile (when enabled in production).
84 const normalizedAnswer = captchaAnswer.trim().toLowerCase();91 const normalizedAnswer = captchaAnswer.trim().toLowerCase();
85 if (normalizedAnswer !== 'point de rencontre') {92 if (normalizedAnswer !== 'point de rencontre') {
86 attemptData.count++;93 attemptData.count++;

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 8

↑↓ moveenter openesc close