detached 6e4a0f0

feat(security): Add development mode bypass for Turnstile token validation

- Implement development mode flag to skip Turnstile token validation
- Update registration and login API routes to handle optional token in dev mode
- Enhance development workflow by simplifying security checks
commit
6e4a0f0264251c775b003a1c92180e44f990338e
author
Kit · · 2025-02-18 16:08 UTC
parent
272f39f
signature
SSH UnverifiedSHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6Tk The key is unknown
3 files · +24 -10
M src/lib/utils/turnstile.server.ts +7 -0
@@ -2,6 +2,7 @@ import { TURNSTILE_SECRET_KEY } from '$env/static/private';
22 import { createLogger } from './logger.server';
33
44 const log = createLogger('turnstile');
5+const isDev = process.env.NODE_ENV === 'development';
56
67 interface TurnstileVerifyResponse {
78 "success": boolean;
@@ -13,6 +14,12 @@ interface TurnstileVerifyResponse {
1314 }
1415
1516 export async function validateTurnstileToken(token: string, remoteip?: string): Promise<boolean> {
17+ // Always return true in development mode
18+ if (isDev) {
19+ log.debug('Development mode: bypassing Turnstile validation');
20+ return true;
21+ }
22+
1623 try {
1724 const formData = new FormData();
1825 formData.append('secret', TURNSTILE_SECRET_KEY);
@@ -2,6 +2,7 @@ import { TURNSTILE_SECRET_KEY } from '$env/static/private';
2 import { createLogger } from './logger.server';2 import { createLogger } from './logger.server';
3 3
4 const log = createLogger('turnstile');4 const log = createLogger('turnstile');
5+const isDev = process.env.NODE_ENV === 'development';
5 6
6 interface TurnstileVerifyResponse {7 interface TurnstileVerifyResponse {
7 "success": boolean;8 "success": boolean;
@@ -13,6 +14,12 @@ interface TurnstileVerifyResponse {
13 }14 }
14 15
15 export async function validateTurnstileToken(token: string, remoteip?: string): Promise<boolean> {16 export async function validateTurnstileToken(token: string, remoteip?: string): Promise<boolean> {
17+ // Always return true in development mode
18+ if (isDev) {
19+ log.debug('Development mode: bypassing Turnstile validation');
20+ return true;
21+ }
22+
16 try {23 try {
17 const formData = new FormData();24 const formData = new FormData();
18 formData.append('secret', TURNSTILE_SECRET_KEY);25 formData.append('secret', TURNSTILE_SECRET_KEY);
M src/routes/api/register/+server.ts +7 -5
@@ -8,6 +8,7 @@ import { createLogger } from '$lib/utils/logger.server';
88 import { eq } from 'drizzle-orm/sql';
99
1010 const log = createLogger('register-server');
11+const isDev = process.env.NODE_ENV === 'development';
1112
1213 // In-memory map to track failed captcha attempts per IP
1314 const captchaAttempts = new Map<string, { count: number, lastAttempt: number }>();
@@ -50,22 +51,23 @@ export const POST: RequestHandler = async ({ request }) => {
5051 suPassword: string,
5152 suConfirmPassword: string,
5253 captchaAnswer: string,
53- turnstileToken: string
54+ turnstileToken?: string
5455 };
5556
56- if (!suUsername || !suPassword || !suConfirmPassword || !captchaAnswer || !turnstileToken) {
57+ if (!suUsername || !suPassword || !suConfirmPassword || !captchaAnswer || (!isDev && !turnstileToken)) {
5758 log.warn('Missing required fields', {
5859 hasUsername: Boolean(suUsername),
5960 hasPassword: Boolean(suPassword),
6061 hasConfirmPassword: Boolean(suConfirmPassword),
6162 hasCaptcha: Boolean(captchaAnswer),
62- hasTurnstileToken: Boolean(turnstileToken)
63+ hasTurnstileToken: Boolean(turnstileToken),
64+ isDev
6365 });
6466 return new Response(JSON.stringify({ error: 'All fields are required' } as RegisterResponseError), { status: 400 });
6567 }
6668
67- // Validate Turnstile token
68- const isValidTurnstile = await validateTurnstileToken(turnstileToken, ip);
69+ // Validate Turnstile token (will be automatically bypassed in dev mode)
70+ const isValidTurnstile = await validateTurnstileToken(turnstileToken || '', ip);
6971 if (!isValidTurnstile) {
7072 log.warn('Invalid Turnstile token', { maskedIp });
7173 return new Response(JSON.stringify({ error: 'Security check failed. Please try again.' } as RegisterResponseError), { status: 400 });
@@ -8,6 +8,7 @@ import { createLogger } from '$lib/utils/logger.server';
8 import { eq } from 'drizzle-orm/sql';8 import { eq } from 'drizzle-orm/sql';
9 9
10 const log = createLogger('register-server');10 const log = createLogger('register-server');
11+const isDev = process.env.NODE_ENV === 'development';
11 12
12 // In-memory map to track failed captcha attempts per IP13 // In-memory map to track failed captcha attempts per IP
13 const captchaAttempts = new Map<string, { count: number, lastAttempt: number }>();14 const captchaAttempts = new Map<string, { count: number, lastAttempt: number }>();
@@ -50,22 +51,23 @@ export const POST: RequestHandler = async ({ request }) => {
50 suPassword: string, 51 suPassword: string,
51 suConfirmPassword: string, 52 suConfirmPassword: string,
52 captchaAnswer: string,53 captchaAnswer: string,
53- turnstileToken: string 54+ turnstileToken?: string
54 };55 };
55 56
56- if (!suUsername || !suPassword || !suConfirmPassword || !captchaAnswer || !turnstileToken) {57+ if (!suUsername || !suPassword || !suConfirmPassword || !captchaAnswer || (!isDev && !turnstileToken)) {
57 log.warn('Missing required fields', { 58 log.warn('Missing required fields', {
58 hasUsername: Boolean(suUsername),59 hasUsername: Boolean(suUsername),
59 hasPassword: Boolean(suPassword),60 hasPassword: Boolean(suPassword),
60 hasConfirmPassword: Boolean(suConfirmPassword),61 hasConfirmPassword: Boolean(suConfirmPassword),
61 hasCaptcha: Boolean(captchaAnswer),62 hasCaptcha: Boolean(captchaAnswer),
62- hasTurnstileToken: Boolean(turnstileToken)63+ hasTurnstileToken: Boolean(turnstileToken),
64+ isDev
63 });65 });
64 return new Response(JSON.stringify({ error: 'All fields are required' } as RegisterResponseError), { status: 400 });66 return new Response(JSON.stringify({ error: 'All fields are required' } as RegisterResponseError), { status: 400 });
65 }67 }
66 68
67- // Validate Turnstile token69+ // Validate Turnstile token (will be automatically bypassed in dev mode)
68- const isValidTurnstile = await validateTurnstileToken(turnstileToken, ip);70+ const isValidTurnstile = await validateTurnstileToken(turnstileToken || '', ip);
69 if (!isValidTurnstile) {71 if (!isValidTurnstile) {
70 log.warn('Invalid Turnstile token', { maskedIp });72 log.warn('Invalid Turnstile token', { maskedIp });
71 return new Response(JSON.stringify({ error: 'Security check failed. Please try again.' } as RegisterResponseError), { status: 400 });73 return new Response(JSON.stringify({ error: 'Security check failed. Please try again.' } as RegisterResponseError), { status: 400 });
M src/routes/api/session/login/+server.ts +10 -5
@@ -11,6 +11,7 @@ import { createLogger } from '$lib/utils/logger.server';
1111 import { validateTurnstileToken } from '$lib/utils/turnstile.server';
1212
1313 const log = createLogger('login-server');
14+const isDev = process.env.NODE_ENV === 'development';
1415
1516 // In-memory map to track failed login attempts per IP
1617 const loginAttempts = new Map<string, { count: number, lastAttempt: number }>();
@@ -75,7 +76,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
7576 const cookieHeader = request.headers.get('cookie');
7677 const clearanceCookie = getClearanceCookie(cookieHeader);
7778
78- // Note: Mark turnstileToken as optional if a valid clearance cookie exists.
79+ // Note: Mark turnstileToken as optional if a valid clearance cookie exists or in dev mode.
7980 const { username, password, turnstileToken } = body as {
8081 username: string;
8182 password: string;
@@ -85,12 +86,13 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
8586 if (
8687 typeof username !== 'string' ||
8788 typeof password !== 'string' ||
88- (!clearanceCookie && typeof turnstileToken !== 'string')
89+ (!isDev && !clearanceCookie && typeof turnstileToken !== 'string')
8990 ) {
9091 log.warn('Missing or invalid input fields', {
9192 hasUsername: typeof username === 'string',
9293 hasPassword: typeof password === 'string',
93- hasTurnstileToken: typeof turnstileToken === 'string'
94+ hasTurnstileToken: typeof turnstileToken === 'string',
95+ isDev
9496 });
9597 return new Response(
9698 JSON.stringify({ error: 'Missing or invalid input fields' } as LoginResponseError),
@@ -98,9 +100,12 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
98100 );
99101 }
100102
101- // Check for clearance cookie before validating Turnstile token.
103+ // Check for dev mode or clearance cookie before validating Turnstile token.
102104 let isValidTurnstile = false;
103- if (clearanceCookie) {
105+ if (isDev) {
106+ log.debug('Development mode: bypassing token challenge.');
107+ isValidTurnstile = true;
108+ } else if (clearanceCookie) {
104109 log.debug('Clearance cookie detected; bypassing token challenge.');
105110 isValidTurnstile = true;
106111 } else {
@@ -11,6 +11,7 @@ import { createLogger } from '$lib/utils/logger.server';
11 import { validateTurnstileToken } from '$lib/utils/turnstile.server';11 import { validateTurnstileToken } from '$lib/utils/turnstile.server';
12 12
13 const log = createLogger('login-server');13 const log = createLogger('login-server');
14+const isDev = process.env.NODE_ENV === 'development';
14 15
15 // In-memory map to track failed login attempts per IP16 // In-memory map to track failed login attempts per IP
16 const loginAttempts = new Map<string, { count: number, lastAttempt: number }>();17 const loginAttempts = new Map<string, { count: number, lastAttempt: number }>();
@@ -75,7 +76,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
75 const cookieHeader = request.headers.get('cookie');76 const cookieHeader = request.headers.get('cookie');
76 const clearanceCookie = getClearanceCookie(cookieHeader);77 const clearanceCookie = getClearanceCookie(cookieHeader);
77 78
78- // Note: Mark turnstileToken as optional if a valid clearance cookie exists.79+ // Note: Mark turnstileToken as optional if a valid clearance cookie exists or in dev mode.
79 const { username, password, turnstileToken } = body as {80 const { username, password, turnstileToken } = body as {
80 username: string;81 username: string;
81 password: string;82 password: string;
@@ -85,12 +86,13 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
85 if (86 if (
86 typeof username !== 'string' ||87 typeof username !== 'string' ||
87 typeof password !== 'string' ||88 typeof password !== 'string' ||
88- (!clearanceCookie && typeof turnstileToken !== 'string')89+ (!isDev && !clearanceCookie && typeof turnstileToken !== 'string')
89 ) {90 ) {
90 log.warn('Missing or invalid input fields', {91 log.warn('Missing or invalid input fields', {
91 hasUsername: typeof username === 'string',92 hasUsername: typeof username === 'string',
92 hasPassword: typeof password === 'string',93 hasPassword: typeof password === 'string',
93- hasTurnstileToken: typeof turnstileToken === 'string'94+ hasTurnstileToken: typeof turnstileToken === 'string',
95+ isDev
94 });96 });
95 return new Response(97 return new Response(
96 JSON.stringify({ error: 'Missing or invalid input fields' } as LoginResponseError),98 JSON.stringify({ error: 'Missing or invalid input fields' } as LoginResponseError),
@@ -98,9 +100,12 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
98 );100 );
99 }101 }
100 102
101- // Check for clearance cookie before validating Turnstile token.103+ // Check for dev mode or clearance cookie before validating Turnstile token.
102 let isValidTurnstile = false;104 let isValidTurnstile = false;
103- if (clearanceCookie) {105+ if (isDev) {
106+ log.debug('Development mode: bypassing token challenge.');
107+ isValidTurnstile = true;
108+ } else if (clearanceCookie) {
104 log.debug('Clearance cookie detected; bypassing token challenge.');109 log.debug('Clearance cookie detected; bypassing token challenge.');
105 isValidTurnstile = true;110 isValidTurnstile = true;
106 } else {111 } else {

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