Guard invalid Turnstile site key
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 46s

This commit is contained in:
Cauê Faleiros
2026-07-28 13:24:48 -03:00
parent c741e520ea
commit 6afd0c0392
4 changed files with 50 additions and 36 deletions

View File

@@ -1,17 +1,19 @@
require('dotenv').config(); require('dotenv').config();
const TURNSTILE_SITE_KEY = const firstEnvValue = (...values) => values.find((value) => typeof value === 'string' && value.trim())?.trim() || '';
process.env.TURNSTILE_SITE_KEY ||
process.env.TURNSTILE_SITEKEY ||
process.env.VITE_TURNSTILE_SITE_KEY ||
process.env.CLOUDFLARE_TURNSTILE_SITE_KEY ||
'';
const TURNSTILE_SECRET = const TURNSTILE_SITE_KEY = firstEnvValue(
process.env.TURNSTILE_SECRET || process.env.TURNSTILE_SITE_KEY,
process.env.TURNSTILE_SECRET_KEY || process.env.TURNSTILE_SITEKEY,
process.env.CLOUDFLARE_TURNSTILE_SECRET || process.env.VITE_TURNSTILE_SITE_KEY,
''; process.env.CLOUDFLARE_TURNSTILE_SITE_KEY
);
const TURNSTILE_SECRET = firstEnvValue(
process.env.TURNSTILE_SECRET,
process.env.TURNSTILE_SECRET_KEY,
process.env.CLOUDFLARE_TURNSTILE_SECRET
);
module.exports = { module.exports = {
PORT: process.env.PORT || 3004, PORT: process.env.PORT || 3004,

View File

@@ -4,6 +4,8 @@ const { TURNSTILE_SECRET, TURNSTILE_SITE_KEY } = require('../config');
const router = express.Router(); const router = express.Router();
const TURNSTILE_VERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'; const TURNSTILE_VERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
const TURNSTILE_SITE_KEY_PATTERN = /^[0-9]x[0-9A-Za-z_-]{20,}$/;
const hasValidTurnstileSiteKey = TURNSTILE_SITE_KEY_PATTERN.test(TURNSTILE_SITE_KEY);
const verifyCaptcha = async (captchaToken, remoteIp) => { const verifyCaptcha = async (captchaToken, remoteIp) => {
if (!TURNSTILE_SECRET) return true; if (!TURNSTILE_SECRET) return true;
@@ -37,7 +39,8 @@ const verifyCaptcha = async (captchaToken, remoteIp) => {
router.get('/login/config', (req, res) => { router.get('/login/config', (req, res) => {
res.json({ res.json({
captchaRequired: Boolean(TURNSTILE_SECRET), captchaRequired: Boolean(TURNSTILE_SECRET),
turnstileSiteKey: TURNSTILE_SITE_KEY turnstileSiteKey: hasValidTurnstileSiteKey ? TURNSTILE_SITE_KEY : '',
captchaConfigured: Boolean(TURNSTILE_SECRET && hasValidTurnstileSiteKey)
}); });
}); });

View File

@@ -57,6 +57,7 @@ const buildDateRangeParams = (dateRange: DateRange) => new URLSearchParams({
export type LoginConfig = { export type LoginConfig = {
captchaRequired: boolean; captchaRequired: boolean;
captchaConfigured: boolean;
turnstileSiteKey: string; turnstileSiteKey: string;
}; };

View File

@@ -27,6 +27,7 @@ const securityUnavailableMessage = 'Não foi possível carregar a verificação
const securityConfigurationMessage = 'Verificação de segurança indisponível. Entre em contato com o administrador.'; const securityConfigurationMessage = 'Verificação de segurança indisponível. Entre em contato com o administrador.';
const securityRequiredMessage = 'Conclua a verificação de segurança para continuar.'; const securityRequiredMessage = 'Conclua a verificação de segurança para continuar.';
const securityFailedMessage = 'Não foi possível validar a verificação de segurança. Atualize a página e tente novamente.'; const securityFailedMessage = 'Não foi possível validar a verificação de segurança. Atualize a página e tente novamente.';
const turnstileSiteKeyPattern = /^[0-9]x[0-9A-Za-z_-]{20,}$/;
const Login = () => { const Login = () => {
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
@@ -41,8 +42,9 @@ const Login = () => {
const captchaContainerRef = useRef<HTMLDivElement | null>(null); const captchaContainerRef = useRef<HTMLDivElement | null>(null);
const captchaWidgetIdRef = useRef<string | null>(null); const captchaWidgetIdRef = useRef<string | null>(null);
const navigate = useNavigate(); const navigate = useNavigate();
const captchaEnabled = Boolean(captchaRequired && turnstileSiteKey); const hasValidSiteKey = turnstileSiteKeyPattern.test(turnstileSiteKey);
const securityMisconfigured = captchaRequired && !turnstileSiteKey; const captchaEnabled = Boolean(captchaRequired && hasValidSiteKey);
const securityMisconfigured = captchaRequired && !hasValidSiteKey;
useEffect(() => { useEffect(() => {
let isMounted = true; let isMounted = true;
@@ -53,9 +55,9 @@ const Login = () => {
if (!isMounted) return; if (!isMounted) return;
setCaptchaRequired(config.captchaRequired); setCaptchaRequired(config.captchaRequired);
setTurnstileSiteKey(config.turnstileSiteKey); setTurnstileSiteKey(config.turnstileSiteKey.trim());
setCaptchaReady(!config.captchaRequired); setCaptchaReady(!config.captchaRequired);
if (config.captchaRequired && !config.turnstileSiteKey) { if (config.captchaRequired && !config.captchaConfigured) {
setError(securityConfigurationMessage); setError(securityConfigurationMessage);
} }
} catch { } catch {
@@ -77,13 +79,14 @@ const Login = () => {
}, []); }, []);
useEffect(() => { useEffect(() => {
if (!turnstileSiteKey || !captchaContainerRef.current || captchaWidgetIdRef.current) return; if (!hasValidSiteKey || !captchaContainerRef.current || captchaWidgetIdRef.current) return;
const siteKey = turnstileSiteKey; const siteKey = turnstileSiteKey;
const renderCaptcha = () => { const renderCaptcha = () => {
if (!window.turnstile || !captchaContainerRef.current || captchaWidgetIdRef.current) return; if (!window.turnstile || !captchaContainerRef.current || captchaWidgetIdRef.current) return;
try {
captchaWidgetIdRef.current = window.turnstile.render(captchaContainerRef.current, { captchaWidgetIdRef.current = window.turnstile.render(captchaContainerRef.current, {
sitekey: siteKey, sitekey: siteKey,
theme: 'dark', theme: 'dark',
@@ -102,6 +105,11 @@ const Login = () => {
}, },
}); });
setCaptchaReady(true); setCaptchaReady(true);
} catch {
setCaptchaToken('');
setCaptchaReady(false);
setError(securityConfigurationMessage);
}
}; };
if (window.turnstile) { if (window.turnstile) {
@@ -128,7 +136,7 @@ const Login = () => {
document.head.appendChild(script); document.head.appendChild(script);
return () => script.removeEventListener('load', renderCaptcha); return () => script.removeEventListener('load', renderCaptcha);
}, [turnstileSiteKey]); }, [hasValidSiteKey, turnstileSiteKey]);
const handleLogin = async (e: React.FormEvent) => { const handleLogin = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();