From 37c6439c76315ae76ccbb66b197330f50d6b897f Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Thu, 21 May 2026 10:45:21 +0200 Subject: [PATCH] fix: add rate limiting and feature flag guard to isSurveyResponsePresentAction (ENG-942) - Apply IP rate limit (10/min) to isSurveyResponsePresentAction to prevent email-enumeration oracle attacks against the single-response-per-email feature - Guard the action behind a server-side isSingleResponsePerEmailEnabled check so response presence cannot be probed on surveys where the feature is disabled - Apply IP rate limit (10/min) to validateSurveyPinAction to prevent brute-force PIN guessing Co-Authored-By: Claude Sonnet 4.6 --- apps/web/modules/core/rate-limit/rate-limit-configs.ts | 10 ++++++++++ apps/web/modules/survey/link/actions.ts | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/apps/web/modules/core/rate-limit/rate-limit-configs.ts b/apps/web/modules/core/rate-limit/rate-limit-configs.ts index ea9480a0ac..fc50a5aca9 100644 --- a/apps/web/modules/core/rate-limit/rate-limit-configs.ts +++ b/apps/web/modules/core/rate-limit/rate-limit-configs.ts @@ -30,6 +30,16 @@ export const rateLimitConfigs = { allowedPerInterval: 10, namespace: "action:send-link-survey-email", }, // 10 per hour + isSurveyResponsePresent: { + interval: 60, + allowedPerInterval: 10, + namespace: "action:survey-response-present", + }, // 10 per minute — prevents email-enumeration oracle + validateSurveyPin: { + interval: 60, + allowedPerInterval: 10, + namespace: "action:validate-survey-pin", + }, // 10 per minute — prevents brute-force PIN guessing licenseRecheck: { interval: 60, allowedPerInterval: 5, namespace: "action:license-recheck" }, // 5 per minute }, diff --git a/apps/web/modules/survey/link/actions.ts b/apps/web/modules/survey/link/actions.ts index 26db9e78d5..b156b684fd 100644 --- a/apps/web/modules/survey/link/actions.ts +++ b/apps/web/modules/survey/link/actions.ts @@ -37,6 +37,8 @@ const ZValidateSurveyPinAction = z.object({ export const validateSurveyPinAction = actionClient .inputSchema(ZValidateSurveyPinAction) .action(async ({ parsedInput }) => { + await applyIPRateLimit(rateLimitConfigs.actions.validateSurveyPin); + // Get survey data which includes pin information const survey = await getSurveyWithMetadata(parsedInput.surveyId); if (!survey) { @@ -62,5 +64,13 @@ const ZIsSurveyResponsePresentAction = z.object({ export const isSurveyResponsePresentAction = actionClient .inputSchema(ZIsSurveyResponsePresentAction) .action(async ({ parsedInput }) => { + await applyIPRateLimit(rateLimitConfigs.actions.isSurveyResponsePresent); + + const survey = await getSurveyWithMetadata(parsedInput.surveyId); + + if (!survey.isSingleResponsePerEmailEnabled) { + throw new InvalidInputError("SINGLE_RESPONSE_PER_EMAIL_NOT_ENABLED"); + } + return await isSurveyResponsePresent(parsedInput.surveyId, parsedInput.email)(); });