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();
const TURNSTILE_SITE_KEY =
process.env.TURNSTILE_SITE_KEY ||
process.env.TURNSTILE_SITEKEY ||
process.env.VITE_TURNSTILE_SITE_KEY ||
process.env.CLOUDFLARE_TURNSTILE_SITE_KEY ||
'';
const firstEnvValue = (...values) => values.find((value) => typeof value === 'string' && value.trim())?.trim() || '';
const TURNSTILE_SECRET =
process.env.TURNSTILE_SECRET ||
process.env.TURNSTILE_SECRET_KEY ||
process.env.CLOUDFLARE_TURNSTILE_SECRET ||
'';
const TURNSTILE_SITE_KEY = firstEnvValue(
process.env.TURNSTILE_SITE_KEY,
process.env.TURNSTILE_SITEKEY,
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 = {
PORT: process.env.PORT || 3004,

View File

@@ -4,6 +4,8 @@ const { TURNSTILE_SECRET, TURNSTILE_SITE_KEY } = require('../config');
const router = express.Router();
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) => {
if (!TURNSTILE_SECRET) return true;
@@ -37,7 +39,8 @@ const verifyCaptcha = async (captchaToken, remoteIp) => {
router.get('/login/config', (req, res) => {
res.json({
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 = {
captchaRequired: boolean;
captchaConfigured: boolean;
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 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 turnstileSiteKeyPattern = /^[0-9]x[0-9A-Za-z_-]{20,}$/;
const Login = () => {
const [email, setEmail] = useState('');
@@ -41,8 +42,9 @@ const Login = () => {
const captchaContainerRef = useRef<HTMLDivElement | null>(null);
const captchaWidgetIdRef = useRef<string | null>(null);
const navigate = useNavigate();
const captchaEnabled = Boolean(captchaRequired && turnstileSiteKey);
const securityMisconfigured = captchaRequired && !turnstileSiteKey;
const hasValidSiteKey = turnstileSiteKeyPattern.test(turnstileSiteKey);
const captchaEnabled = Boolean(captchaRequired && hasValidSiteKey);
const securityMisconfigured = captchaRequired && !hasValidSiteKey;
useEffect(() => {
let isMounted = true;
@@ -53,9 +55,9 @@ const Login = () => {
if (!isMounted) return;
setCaptchaRequired(config.captchaRequired);
setTurnstileSiteKey(config.turnstileSiteKey);
setTurnstileSiteKey(config.turnstileSiteKey.trim());
setCaptchaReady(!config.captchaRequired);
if (config.captchaRequired && !config.turnstileSiteKey) {
if (config.captchaRequired && !config.captchaConfigured) {
setError(securityConfigurationMessage);
}
} catch {
@@ -77,31 +79,37 @@ const Login = () => {
}, []);
useEffect(() => {
if (!turnstileSiteKey || !captchaContainerRef.current || captchaWidgetIdRef.current) return;
if (!hasValidSiteKey || !captchaContainerRef.current || captchaWidgetIdRef.current) return;
const siteKey = turnstileSiteKey;
const renderCaptcha = () => {
if (!window.turnstile || !captchaContainerRef.current || captchaWidgetIdRef.current) return;
captchaWidgetIdRef.current = window.turnstile.render(captchaContainerRef.current, {
sitekey: siteKey,
theme: 'dark',
callback: (token) => {
setCaptchaToken(token);
setCaptchaReady(true);
},
'expired-callback': () => {
setCaptchaToken('');
setCaptchaReady(true);
},
'error-callback': () => {
setCaptchaToken('');
setCaptchaReady(false);
setError(securityUnavailableMessage);
},
});
setCaptchaReady(true);
try {
captchaWidgetIdRef.current = window.turnstile.render(captchaContainerRef.current, {
sitekey: siteKey,
theme: 'dark',
callback: (token) => {
setCaptchaToken(token);
setCaptchaReady(true);
},
'expired-callback': () => {
setCaptchaToken('');
setCaptchaReady(true);
},
'error-callback': () => {
setCaptchaToken('');
setCaptchaReady(false);
setError(securityUnavailableMessage);
},
});
setCaptchaReady(true);
} catch {
setCaptchaToken('');
setCaptchaReady(false);
setError(securityConfigurationMessage);
}
};
if (window.turnstile) {
@@ -128,7 +136,7 @@ const Login = () => {
document.head.appendChild(script);
return () => script.removeEventListener('load', renderCaptcha);
}, [turnstileSiteKey]);
}, [hasValidSiteKey, turnstileSiteKey]);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();