Skip to content

Add Cloudflare Turnstile to grant applications - #909

Open
dergigi wants to merge 18 commits into
masterfrom
feat/turnstile-grant-apply
Open

Add Cloudflare Turnstile to grant applications#909
dergigi wants to merge 18 commits into
masterfrom
feat/turnstile-grant-apply

Conversation

@dergigi

@dergigi dergigi commented Aug 8, 2026

Copy link
Copy Markdown
Member

Adds Cloudflare Turnstile to the grant application form. Submit stays disabled until the challenge succeeds, then /api/github and /api/sendgrid verify the token server-side before creating the issue or sending mail. CSP is updated so the widget can load.

  • embeds cf-turnstile on the final apply step
  • refreshes the token between GitHub and SendGrid (single-use)
  • requires NEXT_PUBLIC_TURNSTILE_SITE_KEY and TURNSTILE_SECRET

Build preview:

dergigi added 4 commits August 8, 2026 10:39
Canonical fail-closed verification against challenges.cloudflare.com
using TURNSTILE_SECRET and the request token/IP.
Gate `/api/github` and `/api/sendgrid` on canonical siteverify before
creating issues or sending mail. Strip the token from the email body.
Show the challenge on the final step, require a token before submit,
and refresh the token between GitHub and SendGrid siteverify calls.
Document NEXT_PUBLIC_TURNSTILE_SITE_KEY and TURNSTILE_SECRET, and reject
unverified requests in the SendGrid and GitHub API suites.
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
os-website Ready Ready Preview Aug 8, 2026 1:20pm

Request Review

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Cloudflare Turnstile bot protection to the shared grant application flow, enforcing server-side verification on both application submission endpoints while keeping the client submit button gated on token readiness.

Changes:

  • Introduces shared Turnstile utilities for token extraction, client IP detection, and canonical siteverify (fail-closed).
  • Updates client submission flow to require Turnstile and refresh the challenge between /api/github and /api/sendgrid calls (single-use tokens).
  • Gates both API routes with Turnstile verification, strips the token from the SendGrid internal email body, and adds Jest coverage.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
utils/turnstile.ts Adds shared Turnstile helpers (token extraction, IP detection, siteverify, API-route assertion).
utils/turnstile.test.js Unit tests for Turnstile helpers and fail-closed behavior.
utils/application-submission.ts Submits with Turnstile tokens; refreshes token between GitHub and SendGrid submissions.
utils/application-submission.test.js Tests fresh-token behavior and token-required gating in submission flow.
tests/api/sendgrid.test.js Adds Turnstile gating tests and asserts token is removed from internal email content.
tests/api/github.test.js Adds Turnstile gating tests for GitHub issue creation endpoint.
pages/api/sendgrid.ts Enforces Turnstile verification and filters token out of the internal email body.
pages/api/github.ts Enforces Turnstile verification before creating GitHub issues.
components/grant-application/TurnstileWidget.tsx New explicit-render Turnstile widget with imperative handle (wait/reset).
components/grant-application/MultiStepApplicationForm.tsx Renders Turnstile on the final step and disables submit until verification is ready.
.env.example Documents NEXT_PUBLIC_TURNSTILE_SITE_KEY and TURNSTILE_SECRET.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread components/grant-application/MultiStepApplicationForm.tsx
Comment thread components/grant-application/TurnstileWidget.tsx Outdated
Reset no longer enqueues waiters on fire-and-forget paths, and leaving
the final step clears turnstileReady so submit stays gated.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.

Suppressed comments (4)

utils/application-submission.ts:67

  • githubToken falls back to String(data[TURNSTILE_TOKEN_FIELD] || ''), which treats non-string values (e.g. an object) as a non-empty token ("[object Object]") and skips the "complete the bot verification" error, leading to a misleading submission failure. Since the submission data is unknown, the fallback should only accept a non-empty string token.
  const githubToken = turnstile
    ? await turnstile.waitForToken()
    : String(data[TURNSTILE_TOKEN_FIELD] || '')

utils/turnstile.ts:60

  • verifyTurnstileToken has no timeout; if Cloudflare stalls, the API route can hang until the platform kills the request, tying up resources and increasing tail latency. Add a short abort timeout so verification fails closed quickly on slow upstream responses.
    const response = await fetchImpl(
      'https://challenges.cloudflare.com/turnstile/v0/siteverify',
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },

pages/api/sendgrid.ts:277

  • If TURNSTILE_SECRET is missing/misconfigured, assertTurnstile returns false and the handler responds 403, which is indistinguishable from a real bot failure and will cause users to retry indefinitely. Consider treating a missing secret as an internal misconfiguration (500) and only returning 403 when verification genuinely fails.
  if (!(await assertTurnstile(req))) {
    return res.status(403).json({ message: TURNSTILE_FAILURE_MESSAGE })
  }

  if (!SENDGRID_API_KEY || !TO_ADDRESS || !FROM_ADDRESS) {
    throw new Error('Env misconfigured')

pages/api/github.ts:22

  • If TURNSTILE_SECRET is missing/misconfigured, assertTurnstile returns false and this route responds 403, which will look like a user/bot problem rather than an internal deployment problem. Consider validating required env (including TURNSTILE_SECRET) first and only returning 403 when siteverify actually fails.
    if (!(await assertTurnstile(req))) {
      return res.status(403).json({ message: TURNSTILE_FAILURE_MESSAGE })
    }

    if (!GH_ACCESS_TOKEN || !GH_ORG || !GH_APP_REPO) {
      throw new Error('Env misconfigured')

Explicit render never initialized when Script onLoad missed under
Next.js dynamic imports, leaving submit disabled with no widget.
script-src and frame-src blocked challenges.cloudflare.com, so the
widget never loaded on apply forms.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (2)

utils/application-submission.ts:85

  • Same issue as githubToken: String(data[TURNSTILE_TOKEN_FIELD] || '') can produce a non-empty string for non-string values. Restrict this to a non-empty string token.
  const emailToken = turnstile
    ? await turnstile.resetAndWaitForToken()
    : String(data[TURNSTILE_TOKEN_FIELD] || '')

utils/application-submission.ts:67

  • When turnstile controls aren't provided, String(data[TURNSTILE_TOKEN_FIELD] || '') can turn non-string values into a non-empty token (e.g. [object Object]), which bypasses the "complete the challenge" check and leads to a confusing server-side 403. Only accept a non-empty string here.

This issue also appears on line 82 of the same file.

  const githubToken = turnstile
    ? await turnstile.waitForToken()
    : String(data[TURNSTILE_TOKEN_FIELD] || '')

@BoltTouring

BoltTouring commented Aug 8, 2026

Copy link
Copy Markdown
Contributor
  • If you click the cloudflare box, then hit back and hit review application again, there is no box and you can't submit.
  • Same thing happens if you don't click the box so clicking back on the final screen removes the cloudflare check.
  • Happens for both General and Red applications
image

@BoltTouring

BoltTouring commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Tried submitting after clicking verify (happy path) and it didn't work. The green check disappeared and the verify box comes back, and it also shows as submitting.
image

If I click the box again it submits (without clicking the orange button) and I receive an email notification. Also shows up in Github.

Explicit-render the widget so leaving and returning to the last step
shows a fresh challenge. Send application emails from /api/github after
issue create so the form no longer waits for a second mid-submit verify.
@dergigi

dergigi commented Aug 8, 2026

Copy link
Copy Markdown
Member Author

Thanks @BoltTouring - should be fixed now/soon, please have another look.

We are in no rush to merge this btw, applications should be fine, this is just a proper anti-spam measure. Even if we merge this on Monday (or even Wednesday or whatever) - that's soon enough.

Arvin21M
Arvin21M previously approved these changes Aug 8, 2026

@Arvin21M Arvin21M left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested on mobile view:

  • We should consider adding "Security Auditing/Testing" as an option under the "Main Focus" dropdown if we want to accept Red Team or similar applications through this channel as well.
  • I encountered Cloudflare error code 600010 during testing. This may be related to using the real configuration in the build preview instead of dummy/test credentials, so may not be an issue once pushed to production.
  • Console message screenshot attached for reference fwiw.
  • Test application is not coming through on the back end, likely due to the cloudflare fail/error issue described above (which is good, anti-spam wise).
  • Otherwise, the application experience looks good from a user perspective, :shipit:
Image

@dergigi
dergigi dismissed Arvin21M’s stale review August 8, 2026 15:41

Superseded by follow-up Security grant focus work in PR #911.

@dergigi

dergigi commented Aug 8, 2026

Copy link
Copy Markdown
Member Author

Let's make sure this works properly before we merge.

@BoltTouring

Copy link
Copy Markdown
Contributor

All of the issues I found have been resolved. LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants