From 1dac22cf8eca7ffa19bd6f4f6a097780ac330611 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 19:25:38 +0000 Subject: [PATCH 1/4] Bound the Turnstile retry and classify Siteverify failures Comparing the runner protection with the canonical Siteverify handler in Cloudflare's Turnstile Spin skill turned up an unbounded loop and a monitoring blind spot that make each other worse. - runner.js re-challenged whenever a response carried the challenge marker, including right after sending a token. A Siteverify that kept rejecting tokens (wrong secret, hostname, or action) turned one Run into endless solves, Worker POSTs, and Siteverify calls. Each Run now earns at most one challenge. When the retried token is rejected, the run ends with the server's message. - Siteverify calls use a 10-second AbortSignal.timeout. Tokens over the documented 2048 characters are rejected without a subrequest. A missing or non-string hostname fails closed instead of raising a 500. - The wide event now records "challenged" apart from "fail". Failures carry a closed-vocabulary reason and allowlisted Siteverify error_codes. Siteverify returns invalid-input-secret with HTTP 400, so error codes are read from non-2xx bodies; a non-2xx never passes. - learner_report.py breaks failures down by reason and code and raises a configuration alert for a wrong or missing secret or site key. Deployment smoke cannot catch these because it uses the bypass header. The browser check now drives a stub server that always rejects. It fails on the old runner (20 POSTs, 19 solves for one Run) and passes on the new one (2 POSTs, 1 solve). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PTpdP6DbuVU4VPLRzjNYGu --- ...96b105eef218.js => runner.05ddb60aa9e7.js} | 4 + public/runner.js | 4 + scripts/check_browser_layout.mjs | 56 +++++ scripts/learner_report.py | 42 ++++ src/asset_manifest.py | 4 +- src/main.py | 129 +++++++++--- tests/test_learner_report.py | 53 +++++ tests/test_main_turnstile.py | 194 ++++++++++++++---- 8 files changed, 407 insertions(+), 79 deletions(-) rename public/{runner.96b105eef218.js => runner.05ddb60aa9e7.js} (97%) diff --git a/public/runner.96b105eef218.js b/public/runner.05ddb60aa9e7.js similarity index 97% rename from public/runner.96b105eef218.js rename to public/runner.05ddb60aa9e7.js index 52e2a6f..cdff252 100644 --- a/public/runner.96b105eef218.js +++ b/public/runner.05ddb60aa9e7.js @@ -210,6 +210,10 @@ function initializeRunner() { const document = new DOMParser().parseFromString(html, 'text/html'); const challengeRequired = document.querySelector('[data-turnstile-required]'); if (challengeRequired) { + // Each Run earns one challenge. If the server rejected the token just + // sent, report why instead of solving again: a wrong secret or hostname + // would otherwise loop through solves and Siteverify calls unbounded. + if (turnstileToken) throw new Error(responseErrorMessage(response, html)); outputPanel.querySelector('code').textContent = challengeRequired.textContent || 'Verification required before running edited code…'; const token = await requestTurnstileToken(signal); diff --git a/public/runner.js b/public/runner.js index 52e2a6f..cdff252 100644 --- a/public/runner.js +++ b/public/runner.js @@ -210,6 +210,10 @@ function initializeRunner() { const document = new DOMParser().parseFromString(html, 'text/html'); const challengeRequired = document.querySelector('[data-turnstile-required]'); if (challengeRequired) { + // Each Run earns one challenge. If the server rejected the token just + // sent, report why instead of solving again: a wrong secret or hostname + // would otherwise loop through solves and Siteverify calls unbounded. + if (turnstileToken) throw new Error(responseErrorMessage(response, html)); outputPanel.querySelector('code').textContent = challengeRequired.textContent || 'Verification required before running edited code…'; const token = await requestTurnstileToken(signal); diff --git a/scripts/check_browser_layout.mjs b/scripts/check_browser_layout.mjs index 38d476e..48bf30f 100755 --- a/scripts/check_browser_layout.mjs +++ b/scripts/check_browser_layout.mjs @@ -463,6 +463,57 @@ try { return { scriptAttempts, fetchCount, renderAction, submittedToken, firstFailure, finalOutput }; })()`); + // A server that keeps rejecting tokens (wrong secret, hostname, or action) + // must cost one challenge per Run. The capped stub stops a regressed + // challenge loop so this check reports it instead of hanging. + await client.send('Page.navigate', { url: `${new URL(target).origin}/?browser_turnstile_rejected=${Date.now()}` }); + await waitFor("document.readyState === 'complete'", 'Turnstile rejection fixture page'); + const turnstileRejected = await evaluateValue(`(async () => { + document.body.innerHTML = \` +
+
+ + +
+ + +
+
+

Output

Ready
+
\`; + const nativeFetch = window.fetch; + let solves = 0; + let widgetOptions = null; + window.turnstile = { + render: (_box, options) => { widgetOptions = options; return 9; }, + execute: () => queueMicrotask(() => widgetOptions.callback('rejected-token-' + (++solves))), + remove: () => {}, + reset: () => {}, + }; + let fetchCount = 0; + window.fetch = async () => { + fetchCount += 1; + const html = fetchCount < 20 + ? '
Verification required

Output

Turnstile verification failed. Press Run to try again.
' + : '

Output

loop cap reached
'; + return { ok: true, status: 200, text: async () => html }; + }; + await import(${JSON.stringify(runnerAsset)} + '?rejected=' + Date.now()); + const form = document.querySelector('form.runner-editor'); + const output = () => document.querySelector('.output-panel code')?.textContent || ''; + const run = async () => { + form.requestSubmit(); + for (let i = 0; i < 100 && form.hasAttribute('aria-busy'); i++) { + await new Promise(resolve => setTimeout(resolve, 10)); + } + return { fetchCount, solves, output: output(), busy: form.hasAttribute('aria-busy') }; + }; + const firstRun = await run(); + const secondRun = await run(); + window.fetch = nativeFetch; + return { firstRun, secondRun }; + })()`); + const failures = []; if (interaction.result.value?.ariaLabel !== 'Editable Python example code') { failures.push('CodeMirror editor is missing its accessible name'); @@ -504,6 +555,10 @@ try { if (!fragmentBound.unchanged || !fragmentBound.notice?.includes('invalid or too large')) failures.push('Oversized shared-code fragment was decoded or not announced'); if (turnstileRetry.scriptAttempts !== 2 || !turnstileRetry.firstFailure.includes('press Run to retry') || turnstileRetry.finalOutput !== 'retry succeeded') failures.push('Transient Turnstile CDN failure was cached instead of retried'); if (turnstileRetry.renderAction !== 'run-example' || turnstileRetry.submittedToken !== 'retry-token') failures.push('Turnstile browser action/token contract failed'); + const { firstRun: rejectedRun, secondRun: rejectedRetry } = turnstileRejected; + if (rejectedRun.fetchCount !== 2 || rejectedRun.solves !== 1) failures.push(`Rejected Turnstile token re-challenged in a loop (${rejectedRun.fetchCount} POSTs, ${rejectedRun.solves} solves for one Run)`); + if (!rejectedRun.output.includes('verification failed') || rejectedRun.busy) failures.push('Rejected Turnstile token did not end the run with the server message'); + if (rejectedRetry.fetchCount !== 4 || rejectedRetry.solves !== 2) failures.push('A new Run after a rejected token did not earn exactly one fresh challenge'); console.log(JSON.stringify({ runnerInteraction: interaction.result.value, @@ -519,6 +574,7 @@ try { offlineRunner, fragmentBound, turnstileRetry, + turnstileRejected, heldCdnRequests, }, null, 2)); client.close(); diff --git a/scripts/learner_report.py b/scripts/learner_report.py index 8738a5c..fcbf973 100755 --- a/scripts/learner_report.py +++ b/scripts/learner_report.py @@ -30,6 +30,14 @@ SERVICE = "pythonbyexample" +# Turnstile signals that mean the deployment is misconfigured rather than that +# a visitor failed: every challenged run fails until an operator fixes them. +TURNSTILE_CONFIG_ALERTS = { + "reason:site_key_missing": "TURNSTILE_SITE_KEY is not configured, so challenges cannot render", + "code:missing-input-secret": "Siteverify received no secret", + "code:invalid-input-secret": "TURNSTILE_SECRET_KEY is wrong, rotated, or from another widget", +} + def _looks_like_wide_event(value) -> bool: return isinstance(value, dict) and value.get("service") == SERVICE and "path" in value @@ -79,6 +87,8 @@ def aggregate_events(events: Iterable[dict]) -> dict: journey_views: Counter[str] = Counter() missing_example_paths: Counter[str] = Counter() turnstile_outcomes: Counter[str] = Counter() + turnstile_failure_reasons: Counter[str] = Counter() + turnstile_error_codes: Counter[str] = Counter() runs: dict[str, dict] = defaultdict( lambda: {"total": 0, "edited": 0, "errors": 0, "execution_ms": []} ) @@ -113,6 +123,11 @@ def aggregate_events(events: Iterable[dict]) -> dict: turnstile = event.get("turnstile") if isinstance(turnstile, dict) and turnstile.get("outcome"): turnstile_outcomes[str(turnstile["outcome"])] += 1 + if turnstile.get("reason"): + turnstile_failure_reasons[str(turnstile["reason"])] += 1 + error_codes = turnstile.get("error_codes") + if isinstance(error_codes, list): + turnstile_error_codes.update(str(code) for code in error_codes) run_summary = {} for slug, entry in runs.items(): @@ -130,10 +145,19 @@ def aggregate_events(events: Iterable[dict]) -> dict: "journey_views": dict(journey_views), "missing_example_paths": dict(missing_example_paths), "turnstile_outcomes": dict(turnstile_outcomes), + "turnstile_failure_reasons": dict(turnstile_failure_reasons), + "turnstile_error_codes": dict(turnstile_error_codes), + "turnstile_config_alerts": _turnstile_config_alerts(turnstile_failure_reasons, turnstile_error_codes), "runs": run_summary, } +def _turnstile_config_alerts(reasons: Counter[str], error_codes: Counter[str]) -> dict[str, int]: + signals = Counter({f"reason:{key}": count for key, count in reasons.items()}) + signals.update({f"code:{key}": count for key, count in error_codes.items()}) + return {signal: signals[signal] for signal in TURNSTILE_CONFIG_ALERTS if signals[signal]} + + def _top(counter: dict, limit: int) -> list[tuple[str, int]]: return sorted(counter.items(), key=lambda item: (-item[1], item[0]))[:limit] @@ -182,6 +206,24 @@ def render_report(report: dict, limit: int = 15) -> str: lines.append(f" {count:>6} {outcome}") lines.append("") + if report["turnstile_failure_reasons"]: + lines.append("Turnstile failure reasons") + for reason, count in _top(report["turnstile_failure_reasons"], limit): + lines.append(f" {count:>6} {reason}") + lines.append("") + + if report["turnstile_error_codes"]: + lines.append("Siteverify error codes") + for code, count in _top(report["turnstile_error_codes"], limit): + lines.append(f" {count:>6} {code}") + lines.append("") + + if report["turnstile_config_alerts"]: + lines.append("Turnstile configuration alerts (fix the deployment; these are not bot failures)") + for signal, count in _top(report["turnstile_config_alerts"], limit): + lines.append(f" {count:>6} {signal}: {TURNSTILE_CONFIG_ALERTS[signal]}") + lines.append("") + return "\n".join(lines) diff --git a/src/asset_manifest.py b/src/asset_manifest.py index 6db62b1..ffa331f 100644 --- a/src/asset_manifest.py +++ b/src/asset_manifest.py @@ -1,3 +1,3 @@ # Generated by scripts/fingerprint_assets.py. Do not edit by hand. -ASSET_PATHS = {'SITE_CSS': '/site.07c59ae07f87.css', 'SYNTAX_JS': '/syntax-highlight.743e2535f133.js', 'EDITOR_JS': '/editor.3659b44c8480.js', 'RUNNER_JS': '/runner.96b105eef218.js', 'SEARCH_JS': '/search.e5a5822d9917.js', 'SEARCH_INDEX': '/search-index.332712b2e502.json'} -HTML_CACHE_VERSION = 'ce207aefedb8' +ASSET_PATHS = {'SITE_CSS': '/site.07c59ae07f87.css', 'SYNTAX_JS': '/syntax-highlight.743e2535f133.js', 'EDITOR_JS': '/editor.3659b44c8480.js', 'RUNNER_JS': '/runner.05ddb60aa9e7.js', 'SEARCH_JS': '/search.e5a5822d9917.js', 'SEARCH_INDEX': '/search-index.332712b2e502.json'} +HTML_CACHE_VERSION = '6e28b930c962' diff --git a/src/main.py b/src/main.py index c75dca1..d392933 100644 --- a/src/main.py +++ b/src/main.py @@ -39,6 +39,24 @@ TURNSTILE_VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify" TURNSTILE_ACTION = "run-example" +TURNSTILE_FAILED_MESSAGE = "Turnstile verification failed. Press Run to try again." +# Cloudflare documents tokens as at most 2048 characters; longer values are +# rejected without spending a Siteverify subrequest. +MAX_TURNSTILE_TOKEN_CHARS = 2048 +SITEVERIFY_TIMEOUT_MS = 10_000 +# Siteverify's documented error codes. Only these names reach the wide event, +# so an unexpected payload cannot widen the logged vocabulary. +SITEVERIFY_ERROR_CODES = frozenset( + { + "missing-input-secret", + "invalid-input-secret", + "missing-input-response", + "invalid-input-response", + "bad-request", + "timeout-or-duplicate", + "internal-error", + } +) SMOKE_BYPASS_HEADER = "x-pythonbyexample-smoke-secret" TURNSTILE_CLEARANCE_COOKIE = "pbe_turnstile_clearance" DEFAULT_TURNSTILE_CLEARANCE_SECONDS = 60 * 60 * 8 @@ -48,11 +66,12 @@ MAX_SUBMITTED_BODY_BYTES = 100_000 try: - from js import Object, caches + from js import AbortSignal, Object, caches from js import Request as JsRequest from js import fetch as js_fetch from pyodide.ffi import create_once_callable, jsnull, to_js except ImportError: # Allows editor tooling outside Workers. + AbortSignal = None Object = None JsRequest = None jsnull = None @@ -320,12 +339,16 @@ async def run_example(slug: str, request: Request): turnstile_verified = False if needs_turnstile and not turnstile_token: - if event := _wide_event(request): - event["turnstile"] = {"outcome": "fail"} site_key = _turnstile_site_key(request) message = "Verification required before running edited code." + # Issuing a challenge is the normal first run of a session, not a + # failure; keeping it apart lets reports compare issued vs rejected. + turnstile_event = {"outcome": "challenged"} if not site_key: message = "Turnstile verification is required, but TURNSTILE_SITE_KEY is not configured." + turnstile_event = {"outcome": "fail", "reason": "site_key_missing"} + if event := _wide_event(request): + event["turnstile"] = turnstile_event return _html( render_example_page( example, @@ -337,9 +360,9 @@ async def run_example(slug: str, request: Request): ) if needs_turnstile: - ok, message, turnstile_outcome = await _verify_turnstile(request, turnstile_token) + ok, message, turnstile_event = await _verify_turnstile(request, turnstile_token) if event := _wide_event(request): - event["turnstile"] = {"outcome": turnstile_outcome} + event["turnstile"] = turnstile_event if not ok: return _html( render_example_page( @@ -387,50 +410,88 @@ def _turnstile_not_required_outcome(request: Request) -> str: return "disabled" -async def _verify_turnstile(request: Request, token: str) -> tuple[bool, str, str]: +def _turnstile_failure(reason: str, error_codes: list[str] | None = None) -> tuple[bool, str, dict]: + turnstile_event = {"outcome": "fail", "reason": reason} + if error_codes: + turnstile_event["error_codes"] = error_codes + return False, TURNSTILE_FAILED_MESSAGE, turnstile_event + + +def _siteverify_error_codes(result: dict) -> list[str]: + codes = result.get("error-codes") + if not isinstance(codes, list): + return [] + return sorted( + {code if code in SITEVERIFY_ERROR_CODES else "other" for code in codes if isinstance(code, str)} + ) + + +async def _verify_turnstile(request: Request, token: str) -> tuple[bool, str, dict]: + """Return (ok, learner message, wide-event turnstile fields). + + Failures carry a closed-vocabulary ``reason`` and, for Siteverify + rejections, its documented ``error_codes``: ``invalid-input-secret`` + means the deployment is misconfigured, not that a learner is a bot. + """ secret = _turnstile_secret(request) if not secret: - return True, "", "disabled" + return True, "", {"outcome": "disabled"} if _smoke_bypass_ok(request): - return True, "", "bypass" + return True, "", {"outcome": "bypass"} if not token: - return False, "Turnstile verification is required before running edited code. Please retry.", "fail" + return ( + False, + "Turnstile verification is required before running edited code. Please retry.", + {"outcome": "fail", "reason": "missing_token"}, + ) + if len(token) > MAX_TURNSTILE_TOKEN_CHARS: + return _turnstile_failure("token_too_long") if js_fetch is None or JsRequest is None: - return False, "Turnstile verification is unavailable outside the Cloudflare runtime.", "fail" + return ( + False, + "Turnstile verification is unavailable outside the Cloudflare runtime.", + {"outcome": "fail", "reason": "runtime_unavailable"}, + ) payload = {"secret": secret, "response": token} remote_ip = request.headers.get("CF-Connecting-IP") if remote_ip: payload["remoteip"] = remote_ip - verify_request = JsRequest.new( - TURNSTILE_VERIFY_URL, - _to_js_object( - { - "method": "POST", - "body": urlencode(payload), - "headers": {"Content-Type": "application/x-www-form-urlencoded"}, - } - ), - ) + init = { + "method": "POST", + "body": urlencode(payload), + "headers": {"Content-Type": "application/x-www-form-urlencoded"}, + } + if AbortSignal is not None: + # A hung Siteverify must not hold the learner's run open indefinitely. + init["signal"] = AbortSignal.timeout(SITEVERIFY_TIMEOUT_MS) + verify_request = JsRequest.new(TURNSTILE_VERIFY_URL, _to_js_object(init)) try: response = await js_fetch(verify_request) - if int(getattr(response, "status", 0) or 0) < 200 or int(getattr(response, "status", 0) or 0) >= 300: - raise ValueError("Turnstile Siteverify returned a non-success status") + status = int(getattr(response, "status", 0) or 0) result = json.loads(await response.text()) - if not isinstance(result, dict): - raise TypeError("Turnstile Siteverify returned a non-object payload") - except Exception: # noqa: BLE001 - JS fetch failures must become a generic verification failure - return False, "Turnstile verification failed. Please refresh the challenge and try again.", "fail" - expected_hostname = urlparse(str(request.url)).hostname or "" - if ( - result.get("success") is True - and result.get("hostname", "").lower().rstrip(".") == expected_hostname.lower().rstrip(".") - and result.get("action") == TURNSTILE_ACTION - ): - return True, "", "pass" - return False, "Turnstile verification failed. Please refresh the challenge and try again.", "fail" + except Exception: # noqa: BLE001 - JS fetch failures, timeouts, and unparseable bodies fail closed + return _turnstile_failure("siteverify_unavailable") + if not isinstance(result, dict): + return _turnstile_failure("siteverify_unavailable") + # Siteverify answers a bad or missing secret with HTTP 400 and a JSON body + # naming it, so read error codes before judging the status: discarding + # non-2xx bodies would report a misconfigured deployment as an outage. + status_ok = 200 <= status < 300 + error_codes = _siteverify_error_codes(result) + if not status_ok and not error_codes: + return _turnstile_failure("siteverify_unavailable") + if not status_ok or result.get("success") is not True: + return _turnstile_failure("rejected", error_codes) + hostname = result.get("hostname") + expected_hostname = (urlparse(str(request.url)).hostname or "").lower().rstrip(".") + if not expected_hostname or not isinstance(hostname, str) or hostname.lower().rstrip(".") != expected_hostname: + return _turnstile_failure("hostname_mismatch") + if result.get("action") != TURNSTILE_ACTION: + return _turnstile_failure("action_mismatch") + return True, "", {"outcome": "pass"} async def _read_dynamic_response_text(response) -> tuple[str, bool]: diff --git a/tests/test_learner_report.py b/tests/test_learner_report.py index 389cb63..8cd1803 100644 --- a/tests/test_learner_report.py +++ b/tests/test_learner_report.py @@ -100,5 +100,58 @@ def test_render_report_is_readable(self): self.assertIn("/examples/not-a-real-example", text) +class TurnstileReportTests(unittest.TestCase): + def turnstile_run(self, turnstile): + event = example_run("closures") + event["turnstile"] = turnstile + return event + + def build_report(self): + return aggregate_events( + [ + self.turnstile_run({"outcome": "challenged"}), + self.turnstile_run({"outcome": "challenged"}), + self.turnstile_run({"outcome": "pass"}), + self.turnstile_run( + {"outcome": "fail", "reason": "rejected", "error_codes": ["invalid-input-secret"]} + ), + self.turnstile_run( + {"outcome": "fail", "reason": "rejected", "error_codes": ["timeout-or-duplicate"]} + ), + self.turnstile_run({"outcome": "fail", "reason": "hostname_mismatch"}), + self.turnstile_run({"outcome": "fail", "reason": "site_key_missing"}), + ] + ) + + def test_separates_issued_challenges_from_failures(self): + report = self.build_report() + self.assertEqual(report["turnstile_outcomes"], {"challenged": 2, "pass": 1, "fail": 4}) + self.assertEqual( + report["turnstile_failure_reasons"], + {"rejected": 2, "hostname_mismatch": 1, "site_key_missing": 1}, + ) + self.assertEqual( + report["turnstile_error_codes"], {"invalid-input-secret": 1, "timeout-or-duplicate": 1} + ) + + def test_flags_misconfiguration_apart_from_visitor_failures(self): + report = self.build_report() + self.assertEqual( + report["turnstile_config_alerts"], + {"code:invalid-input-secret": 1, "reason:site_key_missing": 1}, + ) + text = render_report(report) + self.assertIn("Turnstile configuration alerts", text) + self.assertIn("TURNSTILE_SECRET_KEY is wrong", text) + self.assertIn("Siteverify error codes", text) + + def test_visitor_failures_raise_no_configuration_alert(self): + report = aggregate_events( + [self.turnstile_run({"outcome": "fail", "reason": "rejected", "error_codes": ["invalid-input-response"]})] + ) + self.assertEqual(report["turnstile_config_alerts"], {}) + self.assertNotIn("configuration alerts", render_report(report)) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_main_turnstile.py b/tests/test_main_turnstile.py index 52744fb..2d7e0d8 100644 --- a/tests/test_main_turnstile.py +++ b/tests/test_main_turnstile.py @@ -309,82 +309,190 @@ def test_verified_token_sets_clearance_cookie(self): self._stub_run(output="verified-run") async def _fake_verify(request, token): - return True, "ok", "success" + return True, "", {"outcome": "pass"} main._verify_turnstile = _fake_verify env = session_env(TURNSTILE_SITE_KEY="site-key") - response = self._run(post_request(b"code=print(1)&cf-turnstile-response=tok", env=env)) + request = post_request(b"code=print(1)&cf-turnstile-response=tok", env=env) + request.state.wide_event = {"path": "/examples/values"} + response = self._run(request) self.assertEqual(self._ran_code, "print(1)") set_cookie = response.headers.get("set-cookie", "") self.assertIn(f"{main.TURNSTILE_CLEARANCE_COOKIE}=", set_cookie) + self.assertEqual(request.state.wide_event["turnstile"], {"outcome": "pass"}) + + def test_issued_challenge_is_recorded_apart_from_failures(self): + # A challenge is the normal first run of a session; recording it as + # "fail" hid real rejections among ordinary first runs. + self._stub_run() + request = post_request(b"code=print(1)", env=session_env(TURNSTILE_SITE_KEY="site-key")) + request.state.wide_event = {"path": "/examples/values"} + self._run(request) + self.assertEqual(request.state.wide_event["turnstile"], {"outcome": "challenged"}) + + misconfigured = post_request(b"code=print(1)", env=session_env()) + misconfigured.state.wide_event = {"path": "/examples/values"} + self._run(misconfigured) + self.assertEqual( + misconfigured.state.wide_event["turnstile"], {"outcome": "fail", "reason": "site_key_missing"} + ) + + def test_rejected_token_records_reason_and_does_not_run_code(self): + self._ran_code = None + self._stub_run() + + async def _rejecting_verify(request, token): + return main._turnstile_failure("rejected", ["invalid-input-secret"]) + + main._verify_turnstile = _rejecting_verify + request = post_request( + b"code=print(1)&cf-turnstile-response=tok", env=session_env(TURNSTILE_SITE_KEY="site-key") + ) + request.state.wide_event = {"path": "/examples/values"} + response = self._run(request) + body = response.body.decode() + self.assertIsNone(self._ran_code) + self.assertIn(main.TURNSTILE_FAILED_MESSAGE, body) + self.assertIn('data-turnstile-required="true"', body) + self.assertNotIn("set-cookie", response.headers) + self.assertEqual( + request.state.wide_event["turnstile"], + {"outcome": "fail", "reason": "rejected", "error_codes": ["invalid-input-secret"]}, + ) + + +class _SiteverifyResponse: + def __init__(self, status, body): + self.status, self.body = status, body + + async def text(self): + return self.body class TurnstileSiteverifyTests(unittest.TestCase): def setUp(self): - self.saved_fetch = main.js_fetch - self.saved_request = main.JsRequest + self.saved = {name: getattr(main, name) for name in ("js_fetch", "JsRequest", "AbortSignal")} main.JsRequest = type("Request", (), {"new": staticmethod(lambda url, options: (url, options))}) + self.sent = [] def tearDown(self): - main.js_fetch = self.saved_fetch - main.JsRequest = self.saved_request + for name, value in self.saved.items(): + setattr(main, name, value) - def test_transport_status_and_payload_failures_fail_closed(self): - class Response: - def __init__(self, status, body): - self.status, self.body = status, body - async def text(self): - return self.body + def _respond(self, body, status=200): + async def fetch(request): + self.sent.append(request) + return _SiteverifyResponse(status, body) + + main.js_fetch = fetch + + def _verify(self, token="token"): + return asyncio.run(main._verify_turnstile(make_request(env=session_env()), token)) + def test_transport_status_and_payload_failures_fail_closed(self): async def failing(_request): raise RuntimeError("network down") - request = make_request(env=session_env()) for fetch in ( failing, - lambda _request: _awaitable(Response(500, '{"success": true}')), - lambda _request: _awaitable(Response(200, "not json")), - lambda _request: _awaitable(Response(200, "[]")), - lambda _request: _awaitable(Response(200, '{"success": false}')), + lambda _request: _awaitable(_SiteverifyResponse(500, '{"success": true}')), + lambda _request: _awaitable(_SiteverifyResponse(503, "upstream down")), + lambda _request: _awaitable(_SiteverifyResponse(200, "not json")), + lambda _request: _awaitable(_SiteverifyResponse(200, "[]")), ): with self.subTest(fetch=fetch): main.js_fetch = fetch - ok, message, outcome = asyncio.run(main._verify_turnstile(request, "token")) + ok, message, details = self._verify() self.assertFalse(ok) self.assertIn("verification failed", message) - self.assertEqual(outcome, "fail") + self.assertEqual(details, {"outcome": "fail", "reason": "siteverify_unavailable"}) + + def test_siteverify_request_carries_a_timeout_signal(self): + main.AbortSignal = type("AbortSignal", (), {"timeout": staticmethod(lambda ms: ("timeout", ms))}) + self._respond('{"success": true, "hostname": "www.pythonbyexample.dev", "action": "run-example"}') + self._verify() + url, options = self.sent[0] + self.assertEqual(url, main.TURNSTILE_VERIFY_URL) + self.assertEqual(options["signal"], ("timeout", main.SITEVERIFY_TIMEOUT_MS)) + self.assertEqual(main.SITEVERIFY_TIMEOUT_MS, 10_000) + + def test_overlong_token_is_rejected_without_calling_siteverify(self): + self._respond('{"success": true, "hostname": "www.pythonbyexample.dev", "action": "run-example"}') + ok, _, details = self._verify("x" * (main.MAX_TURNSTILE_TOKEN_CHARS + 1)) + self.assertFalse(ok) + self.assertEqual(details, {"outcome": "fail", "reason": "token_too_long"}) + self.assertEqual(self.sent, []) + + ok, _, _ = self._verify("x" * main.MAX_TURNSTILE_TOKEN_CHARS) + self.assertTrue(ok) + self.assertEqual(len(self.sent), 1) def test_successful_siteverify_requires_matching_hostname_and_action(self): - class Response: - status = 200 - async def text(self): - return '{"success": true, "hostname": "www.pythonbyexample.dev", "action": "run-example"}' - - main.js_fetch = lambda _request: _awaitable(Response()) - ok, message, outcome = asyncio.run(main._verify_turnstile(make_request(env=session_env()), "token")) + self._respond('{"success": true, "hostname": "www.pythonbyexample.dev", "action": "run-example"}') + ok, message, details = self._verify() self.assertTrue(ok) self.assertEqual(message, "") - self.assertEqual(outcome, "pass") - - def test_siteverify_rejects_wrong_or_missing_hostname_and_action(self): - payloads = [ - '{"success": true}', - '{"success": true, "hostname": "other.example", "action": "run-example"}', - '{"success": true, "hostname": "www.pythonbyexample.dev", "action": "other-action"}', + self.assertEqual(details, {"outcome": "pass"}) + + def test_siteverify_rejects_wrong_missing_or_non_string_hostname_and_action(self): + cases = [ + ('{"success": true}', "hostname_mismatch"), + ('{"success": true, "hostname": null, "action": "run-example"}', "hostname_mismatch"), + ('{"success": true, "hostname": 7, "action": "run-example"}', "hostname_mismatch"), + ('{"success": true, "hostname": "other.example", "action": "run-example"}', "hostname_mismatch"), + ('{"success": true, "hostname": "www.pythonbyexample.dev", "action": "other-action"}', "action_mismatch"), + ('{"success": true, "hostname": "www.pythonbyexample.dev"}', "action_mismatch"), ] - for payload in payloads: + for payload, reason in cases: with self.subTest(payload=payload): - class Response: - status = 200 - - async def text(self, response_payload=payload): - return response_payload - - main.js_fetch = lambda _request: _awaitable(Response()) - ok, message, outcome = asyncio.run(main._verify_turnstile(make_request(env=session_env()), "token")) + self._respond(payload) + ok, message, details = self._verify() self.assertFalse(ok) self.assertIn("verification failed", message) - self.assertEqual(outcome, "fail") + self.assertEqual(details, {"outcome": "fail", "reason": reason}) + + def test_rejection_records_only_documented_error_codes(self): + cases = [ + ('{"success": false}', {"outcome": "fail", "reason": "rejected"}), + ( + '{"success": false, "error-codes": ["invalid-input-secret"]}', + {"outcome": "fail", "reason": "rejected", "error_codes": ["invalid-input-secret"]}, + ), + ( + '{"success": false, "error-codes": ["timeout-or-duplicate", "x-new-code", 5, {"a": 1}]}', + {"outcome": "fail", "reason": "rejected", "error_codes": ["other", "timeout-or-duplicate"]}, + ), + ('{"success": "true", "error-codes": "bad-request"}', {"outcome": "fail", "reason": "rejected"}), + ] + for payload, expected in cases: + with self.subTest(payload=payload): + self._respond(payload) + ok, _, details = self._verify() + self.assertFalse(ok) + self.assertEqual(details, expected) + + def test_http_400_secret_errors_are_configuration_codes_not_outages(self): + # Live Siteverify returns these exact bodies with HTTP 400. Treating + # every non-2xx as "unavailable" hid invalid-input-secret entirely. + cases = [ + ('{"error-codes":["invalid-input-secret"],"success":false,"messages":[]}', "invalid-input-secret"), + ('{"error-codes":["missing-input-secret"],"success":false,"messages":[]}', "missing-input-secret"), + ] + for payload, code in cases: + with self.subTest(code=code): + self._respond(payload, status=400) + ok, _, details = self._verify() + self.assertFalse(ok) + self.assertEqual(details, {"outcome": "fail", "reason": "rejected", "error_codes": [code]}) + + def test_non_2xx_never_passes_even_when_the_body_claims_success(self): + self._respond( + '{"success": true, "hostname": "www.pythonbyexample.dev", "action": "run-example", "error-codes": ["internal-error"]}', + status=500, + ) + ok, _, details = self._verify() + self.assertFalse(ok) + self.assertEqual(details, {"outcome": "fail", "reason": "rejected", "error_codes": ["internal-error"]}) async def _awaitable(value): From 34fb8adc82e69e196073a1278b92fd5f8971a243 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 19:25:38 +0000 Subject: [PATCH 2/4] Record the Turnstile Spin lessons Document the challenge/fail split, failure reasons, and error codes in the observability spec and learner-analytics guide. Add the one- challenge-per-Run rule and the reference-contract checks to the runner-protection spec. Record four lessons: bound self-re-entering retries; give a bypassed check its own signal, verified against the live service; check hand-rolled security code against the vendor's reference contract; and treat credential-bearing Wrangler updates as supply-chain changes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PTpdP6DbuVU4VPLRzjNYGu --- CHANGELOG.md | 3 +++ docs/learner-analytics.md | 8 +++++++- docs/lessons-learned.md | 4 ++++ docs/observability-spec.md | 10 +++++++--- docs/turnstile-runner-protection-spec.md | 4 ++++ 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9c2dcd..dfca56d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/en/1.1.0 ### Fixed +- A Run whose Turnstile token the server rejects now ends with the server's message instead of re-challenging. The runner used to solve again every time the response carried the challenge marker, so a persistently failing Siteverify (wrong secret, hostname, or action) turned one click into an unbounded loop of solves, Worker POSTs, and Siteverify subrequests. Each Run now earns at most one challenge, and the failure message says to press Run again. +- Siteverify calls time out after 10 seconds, tokens longer than Cloudflare's documented 2048 characters are rejected without a subrequest, and a `success: true` response with a missing or non-string `hostname` fails closed instead of raising a 500. The gaps surfaced by comparing `_verify_turnstile` with the canonical handler in Cloudflare's Turnstile Spin skill. - Production Python packages are locked. Pywrangler 1.17.4 vendors a committed, hash-pinned `pylock.toml` instead of resolving an unpinned `fastapi` at deploy time, and `uv.lock` pins the same versions (FastAPI 0.141.1, Starlette 1.7.0, Pydantic 2.10.6) so the test suite runs against exactly what ships. `tests/test_dependency_locks.py` fails on drift, and CI and `make deploy` fail if a sync would change `pylock.toml`. Starlette 1.7.0 also clears the five advisories against the previously tested 1.0.0. - Example-page runner wiring (Run interception, Reset, the share button, and keyboard navigation) no longer waits for the CDN-backed highlighter and editor modules: `runner.js` loads `async`, so a slow or unreachable esm.sh cannot stall it — ordered module scripts otherwise execute strictly after every preceding module settles, including their top-level awaits. @@ -33,6 +35,7 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/en/1.1.0 ### Changed +- The `turnstile` wide-event field distinguishes an issued challenge (`challenged`) from a rejected token (`fail`), and failures carry a closed-vocabulary `reason` plus allowlisted Siteverify `error_codes`. `scripts/learner_report.py` breaks failures down by reason and code and raises a configuration alert for `invalid-input-secret`, `missing-input-secret`, or a missing site key, which deployment smoke cannot catch because it uses the bypass header. Siteverify reports those secret errors with HTTP 400, so error codes are now read from non-2xx bodies instead of being logged as an outage. - Dependency refresh: Pillow 12.3.0 (from 11.3.0; 17 advisories), Pywrangler 1.17.4 (from 1.9.3; requires uv 0.12.3+), Shiki 4.4.3 (from 1.29.2; byte-identical output for every example block in both themes), and CodeMirror state 6.7.6, view 6.43.13, and language 6.12.4. - CI actions moved to their Node 24 releases (checkout, setup-node, and setup-python v7; setup-uv v10.2.0 pinned by commit), clearing the Node.js 20 deprecation warning; npm caching stays off. - Dependabot proposes weekly npm, GitHub Actions, and development-tool (Hypothesis, Pillow, Pywrangler) updates. Runtime packages refresh through `make upgrade-runtime-deps`. diff --git a/docs/learner-analytics.md b/docs/learner-analytics.md index 499d2f0..4b4693e 100644 --- a/docs/learner-analytics.md +++ b/docs/learner-analytics.md @@ -21,7 +21,13 @@ time window: human-validated paths may suggest content candidates, but typos, scanners, and bots are confounders. - **Turnstile outcomes** — challenge and rejection counts used to monitor - runner availability and abuse controls, not learner quality. + runner availability and abuse controls, not learner quality. `challenged` + (a challenge was issued) is counted apart from `fail` (a token was + rejected), and failures break down by reason and Siteverify error code. + A **configuration alert** (`invalid-input-secret`, `missing-input-secret`, + or `site_key_missing`) means every challenged run is failing because of + the deployment, not the visitor. Deployment smoke uses the bypass header, + so this report is where a wrong or rotated secret shows up. ## Getting events diff --git a/docs/lessons-learned.md b/docs/lessons-learned.md index beaa9ec..6c84302 100644 --- a/docs/lessons-learned.md +++ b/docs/lessons-learned.md @@ -131,6 +131,10 @@ git diff --check - **Deployment smoke belongs beside CI, and POST smoke must assert rendered output.** `scripts/smoke_deployment.py` checks rendered Worker pages, runtime-boundary pages, journey pages, prototype review pages, and representative Dynamic Worker POST runs for HTTP failures, exception markers, and stale edited-code output. Build success is not enough; the deployed Worker must render and execute edited examples. With Turnstile enabled, submitted code can appear in the editor textarea even when it did not run, so POST smoke must inspect the output panel rather than searching the whole HTML document. - **Observability smoke should assert the custom event, not the whole tail envelope.** Use unique `x-request-id` values, exercise cache miss/hit/bypass and client-error paths, and assert on the structured payload inside `logs[].message[]`. If Turnstile is enabled, Dynamic Worker error-path probes need the smoke bypass secret; otherwise they only verify the Turnstile-fail path. - **Turnstile should be secret-gated, session-scoped, and invisible until needed.** Protect edited-code POST runs only when `TURNSTILE_SECRET_KEY` and an explicit challenge mode are configured, lazy-load/render the Invisible-mode widget only after the server returns a challenge-required marker, and issue a signed clearance cookie so normal session runs skip Siteverify. The Cloudflare widget mode is `Invisible`; the client-side render option is `execution: "execute"`, not `size: "invisible"`. If production smoke must POST through a protected endpoint, use a separate `PBE_SMOKE_BYPASS_SECRET` header so smoke remains a deployment check rather than a CAPTCHA solver. See `docs/turnstile-runner-protection-spec.md` for the full runner-protection design. +- **A retry that can re-enter itself needs a bound.** The runner re-challenged whenever a response carried the challenge marker, including right after it had sent a token. A server that kept rejecting tokens (wrong secret, hostname, or action) turned one Run into an unbounded loop of Turnstile solves, Worker POSTs, and Siteverify subrequests. Cloudflare's Turnstile Spin skill treats tokens as single-use with at most one reset per retry; the runner now earns one challenge per Run and shows the server's message when the retried token is rejected. `scripts/check_browser_layout.mjs` drives a stub server that always rejects and fails if one Run costs more than one solve. +- **A bypassed check needs its own signal.** Deployment smoke uses `PBE_SMOKE_BYPASS_SECRET`, so it never proves the production `TURNSTILE_SECRET_KEY` works, and the wide event recorded both "challenge issued" and "Siteverify rejected" as `fail`. A wrong or rotated secret would have passed smoke while every new session's first run failed. Turnstile Spin's "Fix with Spin" banner is Cloudflare noticing widgets that serve traffic without working server-side validation; the self-hosted equivalent is `challenged` versus `fail` outcomes plus Siteverify `error_codes`, with `invalid-input-secret` reported by `scripts/learner_report.py` as a configuration alert rather than a bot failure. Verify the signal against the live service, not a stub: real Siteverify returns `invalid-input-secret` and `missing-input-secret` with HTTP 400, so the common `if (!response.ok) throw` pattern (Spin's reference handler included) discards the one code that names the misconfiguration. Unit stubs had assumed a 200. +- **Check hand-rolled security code against the vendor's reference contract.** Reading the Turnstile Spin skill's canonical Siteverify handler beside `_verify_turnstile` found three gaps no test had asked about: no timeout, no 2048-character token bound, and a `null` hostname raising `AttributeError` (a 500) instead of failing closed. Use the reference as a checklist, and keep deliberate divergences written down: Invisible mode, session clearance instead of per-request verification, and the serving hostname instead of a configured allowlist (safe only while `workers.dev` and preview URLs stay disabled). +- **Credential-bearing commands run code from the dependency tree.** Turnstile Spin refuses to run `secret put` or other credential-bearing commands through `npx`, package scripts, or a project-local binary. `make deploy` runs the repository-pinned Wrangler with the account's credentials, and Dependabot now proposes npm updates weekly. Exact pins, the three-day cooldown, and `npm ci --ignore-scripts` limit the exposure; review Wrangler and Miniflare bumps as credential-bearing changes, not routine ones. ## Discoverability, theming, and learner analytics diff --git a/docs/observability-spec.md b/docs/observability-spec.md index 812966e..faa3554 100644 --- a/docs/observability-spec.md +++ b/docs/observability-spec.md @@ -404,7 +404,9 @@ Add: - `example.code_hash` — sha256 hex of submitted UTF-8 bytes, first 12 chars - `example.code_bytes` — `len(submitted.encode("utf-8"))` - `example.code_edited` — `submitted != example["code"]` -- `turnstile.outcome` — `pass` / `fail` / `bypass` / `disabled` +- `turnstile.outcome` — `challenged` / `pass` / `fail` / `bypass` / `disabled`. `challenged` means the request was sent a challenge; it is the normal first run of a session, not a failure. +- `turnstile.reason` — on `fail` only: `rejected` / `hostname_mismatch` / `action_mismatch` / `siteverify_unavailable` / `token_too_long` / `site_key_missing` / `missing_token` / `runtime_unavailable` +- `turnstile.error_codes` — on `rejected` only: Siteverify's documented `error-codes`, sorted, with anything undocumented recorded as `other` - `execution_ms` — duration around `_run_example`, recorded in a `finally` if execution started Example: @@ -419,7 +421,7 @@ request.state.wide_event["example"] = { } ``` -Change `_verify_turnstile(...)` to return `(ok, message, outcome)` so the caller can record the outcome without re-deriving it. Do not log the token or `CF-Connecting-IP`; the latter may still be sent to Turnstile verification but must not enter the event. +`_verify_turnstile(...)` returns `(ok, message, turnstile_fields)` so the caller records the outcome, failure reason, and error codes without re-deriving them. An `invalid-input-secret` code means the deployment is misconfigured, not that a visitor failed. Do not log the token or `CF-Connecting-IP`; the latter may still be sent to Turnstile verification but must not enter the event. #### `_run_example` @@ -567,7 +569,9 @@ Every event carries the context from `observability.py` plus a subset of the per | `example.code_hash` | string | handler | First 12 hex chars of sha256(submitted UTF-8 bytes). | | `example.code_bytes` | int | handler | Byte length of submitted UTF-8 code. | | `example.code_edited` | bool | handler | True when submitted differs from canonical. | -| `turnstile.outcome` | string | handler | `pass` / `fail` / `bypass` / `disabled`. | +| `turnstile.outcome` | string | handler | `challenged` / `pass` / `fail` / `bypass` / `disabled`. | +| `turnstile.reason` | string | handler | Closed-vocabulary failure reason; present only when `outcome` is `fail`. | +| `turnstile.error_codes` | array | handler | Allowlisted Siteverify `error-codes` (`other` for unknown); present only for `rejected`. | | `execution_ms` | float | handler | Sandboxed run duration. | | `worker.outcome` | string | handler | Dynamic Worker outcome. | | `worker.status_code` | int | handler | Dynamic Worker HTTP status when fetch completes. | diff --git a/docs/turnstile-runner-protection-spec.md b/docs/turnstile-runner-protection-spec.md index 581f7cf..5c2650f 100644 --- a/docs/turnstile-runner-protection-spec.md +++ b/docs/turnstile-runner-protection-spec.md @@ -110,6 +110,7 @@ If `TURNSTILE_CHALLENGE_MODE=session` and both site/secret keys are configured: 7. The Worker validates the token through Siteverify. 8. On success, the Worker sets a signed `pbe_turnstile_clearance` cookie. 9. Later runs in that clearance window skip Turnstile and go straight to the Dynamic Worker. +10. If Siteverify rejects the token, the client shows the server's failure message and stops. Each Run earns at most one challenge; pressing Run again earns a fresh one. Siteverify endpoint from Cloudflare docs: @@ -160,6 +161,9 @@ x-pythonbyexample-smoke-secret: - The Turnstile widget should be configured in Cloudflare as **Invisible** mode. Client code uses explicit rendering with `execution: "execute"`; `size: "invisible"` is not a valid current Turnstile size option. - The widget is removed after callback success or failure. - Siteverify is called only when a challenge-required request retries with a token. +- A Run that sends a token and is challenged again does not solve again. Before this rule, a persistently failing Siteverify (wrong secret, hostname, or action) turned one click into an unbounded loop of solves, Worker POSTs, and Siteverify subrequests. +- Siteverify matches Cloudflare's reference contract (and the Turnstile Spin skill's canonical handler): `success is True`, the `run-example` action, the serving hostname, `remoteip` from `CF-Connecting-IP`, a 10-second `AbortSignal.timeout`, tokens over 2048 characters rejected before any subrequest, and every transport, status, payload, or type surprise failing closed. +- Each result lands in the wide event as `turnstile.outcome` (`challenged`, `pass`, `fail`, `bypass`, `disabled`) with a failure `reason` and allowlisted Siteverify `error_codes`, so `scripts/learner_report.py` can separate issued challenges from rejections and flag `invalid-input-secret` as a configuration alert. Siteverify reports a bad or missing secret with HTTP 400 and a JSON body, so the Worker reads error codes from non-2xx bodies too; a non-2xx response still never passes. - A valid challenge creates a signed, HttpOnly, Secure, SameSite=Lax clearance cookie scoped to `/examples`. - Failed or missing verification does not create a Dynamic Worker. - POST responses are still never cached. From eae0dc352bd5892fbd48513e5a8cc554b91989aa Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 22:13:56 +0000 Subject: [PATCH 3/4] Prove the production Turnstile secret during deployment smoke Smoke skips Turnstile with the bypass header, so it never touched Siteverify. A wrong, rotated, or test secret passed smoke while every new session's first run failed. The Worker now exposes POST /__smoke/turnstile, gated by the same header and answering 404 without it. The route sends the configured secret to Siteverify with Cloudflare's documented dummy token, which production secrets reject, and reports the result without the secret: - valid: invalid-input-response, HTTP 200, no testing metadata - invalid: invalid-input-secret or missing-input-secret (HTTP 400) - testing_key: metadata.result_with_testing_key, or success on the dummy token. The always-fail test secret answers exactly like a production secret; only this flag tells them apart. - unverified, unexpected, or absent (Turnstile off) With PBE_SMOKE_BYPASS_SECRET, scripts/smoke_deployment.py calls the probe. It fails unless the secret is a working production secret with a configured site key, or Turnstile is off. Without the secret, it prints SKIP. The Siteverify call now lives in one helper shared by learner verification and the probe. A local Worker returned invalid, testing_key (always-fail and always-pass test secrets), and absent as expected. The valid path is covered by unit tests using Cloudflare's documented reply, since no production secret is available here. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PTpdP6DbuVU4VPLRzjNYGu --- CHANGELOG.md | 4 +- docs/learner-analytics.md | 5 +- docs/lessons-learned.md | 2 +- docs/observability-spec.md | 2 + docs/turnstile-runner-protection-spec.md | 22 ++++ scripts/smoke_deployment.py | 54 ++++++++- src/asset_manifest.py | 2 +- src/main.py | 106 +++++++++++++---- tests/test_main_turnstile.py | 139 +++++++++++++++++++++++ tests/test_smoke_deployment.py | 82 +++++++++++++ 10 files changed, 392 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfca56d..f6e6f58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/en/1.1.0 ### Fixed +- Deployment smoke now proves the production Turnstile secret. Its bypass header skips Siteverify, so a wrong, rotated, or test secret used to pass smoke while every new session's first run failed. With `PBE_SMOKE_BYPASS_SECRET`, `scripts/smoke_deployment.py` calls the header-gated `POST /__smoke/turnstile`. The Worker sends its secret with Cloudflare's dummy token and reports `valid`, `invalid`, `testing_key`, `unverified`, `unexpected`, or `absent`. Smoke fails unless the secret is a working production secret with a site key, or Turnstile is off. + - A Run whose Turnstile token the server rejects now ends with the server's message instead of re-challenging. The runner used to solve again every time the response carried the challenge marker, so a persistently failing Siteverify (wrong secret, hostname, or action) turned one click into an unbounded loop of solves, Worker POSTs, and Siteverify subrequests. Each Run now earns at most one challenge, and the failure message says to press Run again. - Siteverify calls time out after 10 seconds, tokens longer than Cloudflare's documented 2048 characters are rejected without a subrequest, and a `success: true` response with a missing or non-string `hostname` fails closed instead of raising a 500. The gaps surfaced by comparing `_verify_turnstile` with the canonical handler in Cloudflare's Turnstile Spin skill. - Production Python packages are locked. Pywrangler 1.17.4 vendors a committed, hash-pinned `pylock.toml` instead of resolving an unpinned `fastapi` at deploy time, and `uv.lock` pins the same versions (FastAPI 0.141.1, Starlette 1.7.0, Pydantic 2.10.6) so the test suite runs against exactly what ships. `tests/test_dependency_locks.py` fails on drift, and CI and `make deploy` fail if a sync would change `pylock.toml`. Starlette 1.7.0 also clears the five advisories against the previously tested 1.0.0. @@ -35,7 +37,7 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/en/1.1.0 ### Changed -- The `turnstile` wide-event field distinguishes an issued challenge (`challenged`) from a rejected token (`fail`), and failures carry a closed-vocabulary `reason` plus allowlisted Siteverify `error_codes`. `scripts/learner_report.py` breaks failures down by reason and code and raises a configuration alert for `invalid-input-secret`, `missing-input-secret`, or a missing site key, which deployment smoke cannot catch because it uses the bypass header. Siteverify reports those secret errors with HTTP 400, so error codes are now read from non-2xx bodies instead of being logged as an outage. +- The `turnstile` wide-event field distinguishes an issued challenge (`challenged`) from a rejected token (`fail`), and failures carry a closed-vocabulary `reason` plus allowlisted Siteverify `error_codes`. `scripts/learner_report.py` breaks failures down by reason and code and raises a configuration alert for `invalid-input-secret`, `missing-input-secret`, or a missing site key. Siteverify reports those secret errors with HTTP 400, so error codes are now read from non-2xx bodies instead of being logged as an outage. - Dependency refresh: Pillow 12.3.0 (from 11.3.0; 17 advisories), Pywrangler 1.17.4 (from 1.9.3; requires uv 0.12.3+), Shiki 4.4.3 (from 1.29.2; byte-identical output for every example block in both themes), and CodeMirror state 6.7.6, view 6.43.13, and language 6.12.4. - CI actions moved to their Node 24 releases (checkout, setup-node, and setup-python v7; setup-uv v10.2.0 pinned by commit), clearing the Node.js 20 deprecation warning; npm caching stays off. - Dependabot proposes weekly npm, GitHub Actions, and development-tool (Hypothesis, Pillow, Pywrangler) updates. Runtime packages refresh through `make upgrade-runtime-deps`. diff --git a/docs/learner-analytics.md b/docs/learner-analytics.md index 4b4693e..4e9b69e 100644 --- a/docs/learner-analytics.md +++ b/docs/learner-analytics.md @@ -26,8 +26,9 @@ time window: rejected), and failures break down by reason and Siteverify error code. A **configuration alert** (`invalid-input-secret`, `missing-input-secret`, or `site_key_missing`) means every challenged run is failing because of - the deployment, not the visitor. Deployment smoke uses the bypass header, - so this report is where a wrong or rotated secret shows up. + the deployment, not the visitor. Deployment smoke probes the secret at + deploy time; this report catches a secret that breaks afterwards, such as + a rotation in the dashboard. ## Getting events diff --git a/docs/lessons-learned.md b/docs/lessons-learned.md index 6c84302..6e9a944 100644 --- a/docs/lessons-learned.md +++ b/docs/lessons-learned.md @@ -132,7 +132,7 @@ git diff --check - **Observability smoke should assert the custom event, not the whole tail envelope.** Use unique `x-request-id` values, exercise cache miss/hit/bypass and client-error paths, and assert on the structured payload inside `logs[].message[]`. If Turnstile is enabled, Dynamic Worker error-path probes need the smoke bypass secret; otherwise they only verify the Turnstile-fail path. - **Turnstile should be secret-gated, session-scoped, and invisible until needed.** Protect edited-code POST runs only when `TURNSTILE_SECRET_KEY` and an explicit challenge mode are configured, lazy-load/render the Invisible-mode widget only after the server returns a challenge-required marker, and issue a signed clearance cookie so normal session runs skip Siteverify. The Cloudflare widget mode is `Invisible`; the client-side render option is `execution: "execute"`, not `size: "invisible"`. If production smoke must POST through a protected endpoint, use a separate `PBE_SMOKE_BYPASS_SECRET` header so smoke remains a deployment check rather than a CAPTCHA solver. See `docs/turnstile-runner-protection-spec.md` for the full runner-protection design. - **A retry that can re-enter itself needs a bound.** The runner re-challenged whenever a response carried the challenge marker, including right after it had sent a token. A server that kept rejecting tokens (wrong secret, hostname, or action) turned one Run into an unbounded loop of Turnstile solves, Worker POSTs, and Siteverify subrequests. Cloudflare's Turnstile Spin skill treats tokens as single-use with at most one reset per retry; the runner now earns one challenge per Run and shows the server's message when the retried token is rejected. `scripts/check_browser_layout.mjs` drives a stub server that always rejects and fails if one Run costs more than one solve. -- **A bypassed check needs its own signal.** Deployment smoke uses `PBE_SMOKE_BYPASS_SECRET`, so it never proves the production `TURNSTILE_SECRET_KEY` works, and the wide event recorded both "challenge issued" and "Siteverify rejected" as `fail`. A wrong or rotated secret would have passed smoke while every new session's first run failed. Turnstile Spin's "Fix with Spin" banner is Cloudflare noticing widgets that serve traffic without working server-side validation; the self-hosted equivalent is `challenged` versus `fail` outcomes plus Siteverify `error_codes`, with `invalid-input-secret` reported by `scripts/learner_report.py` as a configuration alert rather than a bot failure. Verify the signal against the live service, not a stub: real Siteverify returns `invalid-input-secret` and `missing-input-secret` with HTTP 400, so the common `if (!response.ok) throw` pattern (Spin's reference handler included) discards the one code that names the misconfiguration. Unit stubs had assumed a 200. +- **A bypassed check needs its own signal.** Deployment smoke uses `PBE_SMOKE_BYPASS_SECRET`, so it never proves the production `TURNSTILE_SECRET_KEY` works, and the wide event recorded both "challenge issued" and "Siteverify rejected" as `fail`. A wrong or rotated secret would have passed smoke while every new session's first run failed. Turnstile Spin's "Fix with Spin" banner is Cloudflare noticing widgets that serve traffic without working server-side validation; the self-hosted equivalent is `challenged` versus `fail` outcomes plus Siteverify `error_codes`, with `invalid-input-secret` reported by `scripts/learner_report.py` as a configuration alert rather than a bot failure. Verify the signal against the live service, not a stub: real Siteverify returns `invalid-input-secret` and `missing-input-secret` with HTTP 400, so the common `if (!response.ok) throw` pattern (Spin's reference handler included) discards the one code that names the misconfiguration. Unit stubs had assumed a 200. The deploy-time half of the fix is a smoke-gated probe (`POST /__smoke/turnstile`) that sends the production secret with Cloudflare's dummy token. Classify on more than the error code: Cloudflare's always-fail test secret answers the dummy token with the same `invalid-input-response` as a working production secret, and only `metadata.result_with_testing_key` tells them apart. - **Check hand-rolled security code against the vendor's reference contract.** Reading the Turnstile Spin skill's canonical Siteverify handler beside `_verify_turnstile` found three gaps no test had asked about: no timeout, no 2048-character token bound, and a `null` hostname raising `AttributeError` (a 500) instead of failing closed. Use the reference as a checklist, and keep deliberate divergences written down: Invisible mode, session clearance instead of per-request verification, and the serving hostname instead of a configured allowlist (safe only while `workers.dev` and preview URLs stay disabled). - **Credential-bearing commands run code from the dependency tree.** Turnstile Spin refuses to run `secret put` or other credential-bearing commands through `npx`, package scripts, or a project-local binary. `make deploy` runs the repository-pinned Wrangler with the account's credentials, and Dependabot now proposes npm updates weekly. Exact pins, the three-day cooldown, and `npm ci --ignore-scripts` limit the exposure; review Wrangler and Miniflare bumps as credential-bearing changes, not routine ones. diff --git a/docs/observability-spec.md b/docs/observability-spec.md index faa3554..b8aba0a 100644 --- a/docs/observability-spec.md +++ b/docs/observability-spec.md @@ -572,6 +572,8 @@ Every event carries the context from `observability.py` plus a subset of the per | `turnstile.outcome` | string | handler | `challenged` / `pass` / `fail` / `bypass` / `disabled`. | | `turnstile.reason` | string | handler | Closed-vocabulary failure reason; present only when `outcome` is `fail`. | | `turnstile.error_codes` | array | handler | Allowlisted Siteverify `error-codes` (`other` for unknown); present only for `rejected`. | +| `turnstile_probe.secret` | string | probe handler | `valid` / `invalid` / `testing_key` / `unverified` / `unexpected` / `absent`; only on authenticated `POST /__smoke/turnstile`. | +| `turnstile_probe.ok` | bool | probe handler | Whether the deployed configuration can verify browsers. | | `execution_ms` | float | handler | Sandboxed run duration. | | `worker.outcome` | string | handler | Dynamic Worker outcome. | | `worker.status_code` | int | handler | Dynamic Worker HTTP status when fetch completes. | diff --git a/docs/turnstile-runner-protection-spec.md b/docs/turnstile-runner-protection-spec.md index 5c2650f..cc68798 100644 --- a/docs/turnstile-runner-protection-spec.md +++ b/docs/turnstile-runner-protection-spec.md @@ -191,6 +191,26 @@ x-pythonbyexample-smoke-secret: This is only an app-level bypass. If Cloudflare Rate Limiting challenges smoke traffic before it reaches the Worker, either keep smoke below the threshold or add a Cloudflare-side skip/exception. +Because bypassed runs never touch Siteverify, the same header also unlocks a secret probe: + +```text +POST /__smoke/turnstile +x-pythonbyexample-smoke-secret: +``` + +The Worker sends its `TURNSTILE_SECRET_KEY` to Siteverify with Cloudflare's documented dummy token `XXXX.DUMMY.TOKEN.XXXX` and returns JSON (`Cache-Control: no-store`). It never includes the secret itself. Cloudflare documents that production secrets reject the dummy token; how they reject it tells the configurations apart: + +| Siteverify reply to the dummy token | `secret` | Smoke | +| --- | --- | --- | +| `invalid-input-response`, HTTP 200, no testing metadata | `valid` | passes when a site key is configured | +| `invalid-input-secret` or `missing-input-secret` (HTTP 400) | `invalid` | fails | +| `metadata.result_with_testing_key: true`, or `success: true` | `testing_key` | fails | +| unreachable, timeout, or unparseable | `unverified` | fails; re-run | +| anything else | `unexpected` | fails | +| no `TURNSTILE_SECRET_KEY` configured | `absent` | passes; Turnstile is off | + +The testing-metadata check matters: Cloudflare's always-fail test secret answers the dummy token with the same `invalid-input-response` as a working production secret, so without it a deployment where every learner fails would pass. Without the header, the route answers 404 and never calls Siteverify. The probe proves the secret is a working production secret. It cannot prove that the secret and site key belong to the same widget, or that the widget allows the production hostname; a browser run still covers those. + ## Recommended Cloudflare layer: Rate Limiting Rules The best first production protection for Dynamic Worker cost is Cloudflare Rate Limiting, because it runs before the Worker and limits repeated runner traffic at the edge. @@ -455,6 +475,8 @@ Deployment smoke OK The smoke script checks the rendered output panel for POST runs. This is important because a Turnstile-required response can echo submitted code in the editor textarea without executing it. +With `PBE_SMOKE_BYPASS_SECRET`, smoke also calls `POST /__smoke/turnstile` and fails unless the Worker's secret is a working production secret with a configured site key (see "Smoke-test behavior"). Without the secret, it prints `SKIP` for the probe. + ## Verification checklist ### Verify no visible widget on page load diff --git a/scripts/smoke_deployment.py b/scripts/smoke_deployment.py index cd4345b..eb43289 100755 --- a/scripts/smoke_deployment.py +++ b/scripts/smoke_deployment.py @@ -3,11 +3,16 @@ Usage: scripts/smoke_deployment.py https://www.pythonbyexample.dev + PBE_SMOKE_BYPASS_SECRET=... scripts/smoke_deployment.py https://www.pythonbyexample.dev + +With PBE_SMOKE_BYPASS_SECRET, POST runs skip Turnstile, so the script also +asks the Worker to prove its Turnstile secret against Siteverify. """ from __future__ import annotations import argparse import html +import json import os import re import sys @@ -35,6 +40,14 @@ ("subprocesses", "print('runtime-smoke-subprocess-boundary')\n", "runtime-smoke-subprocess-boundary"), ] ERROR_MARKERS = ["error code: 1101", "PythonError", "Traceback"] +SMOKE_BYPASS_HEADER = "x-pythonbyexample-smoke-secret" +TURNSTILE_PROBE_PATH = "/__smoke/turnstile" +TURNSTILE_PROBE_PROBLEMS = { + "invalid": "Siteverify rejects TURNSTILE_SECRET_KEY, so every challenged run fails", + "testing_key": "TURNSTILE_SECRET_KEY is a Cloudflare test secret, which ignores the visitor", + "unverified": "the Worker could not reach Siteverify; re-run smoke before trusting the secret", + "unexpected": "Siteverify answered the dummy token unexpectedly", +} def fetch(url: str) -> tuple[int, str]: @@ -51,7 +64,7 @@ def post_code(url: str, code: str, smoke_bypass_secret: str = "") -> tuple[int, "Content-Type": "application/x-www-form-urlencoded", } if smoke_bypass_secret: - headers["x-pythonbyexample-smoke-secret"] = smoke_bypass_secret + headers[SMOKE_BYPASS_HEADER] = smoke_bypass_secret request = urllib.request.Request( url, data=data, @@ -63,6 +76,30 @@ def post_code(url: str, code: str, smoke_bypass_secret: str = "") -> tuple[int, return response.status, body +def probe_turnstile(base_url: str, smoke_bypass_secret: str) -> dict: + request = urllib.request.Request( + urljoin(base_url, TURNSTILE_PROBE_PATH.lstrip("/")), + data=b"", + headers={"User-Agent": "pythonbyexample-smoke/1.0", SMOKE_BYPASS_HEADER: smoke_bypass_secret}, + method="POST", + ) + with urllib.request.urlopen(request, timeout=30) as response: + return json.loads(response.read().decode("utf-8")) + + +def turnstile_probe_problem(report: dict) -> str | None: + """Explain why the Worker's Turnstile configuration cannot verify browsers.""" + if report.get("ok") is True: + return None + secret = report.get("secret") + if secret == "valid" and not report.get("site_key_configured"): + return "TURNSTILE_SITE_KEY is not configured, so challenges cannot render" + problem = TURNSTILE_PROBE_PROBLEMS.get(secret, f"unrecognized probe report {report!r}") + if codes := report.get("error_codes"): + problem += f" ({', '.join(codes)})" + return problem + + def has_exception_marker(body: str) -> str | None: lowered = body.lower() for marker in ERROR_MARKERS: @@ -139,6 +176,21 @@ def main() -> int: failures.append(f"POST {url}: missing edited-code output {expected!r}") print(f"POST {status} {url} -> {expected}") + url = urljoin(base, TURNSTILE_PROBE_PATH.lstrip("/")) + if not smoke_bypass_value: + print(f"SKIP {url}: set PBE_SMOKE_BYPASS_SECRET to verify the deployed Turnstile secret") + else: + try: + report = probe_turnstile(base, smoke_bypass_value) + except urllib.error.HTTPError as exc: + failures.append(f"POST {url}: HTTP {exc.code} (deploy the probe route, or check PBE_SMOKE_BYPASS_SECRET)") + except Exception as exc: # noqa: BLE001 # pragma: no cover - report any transport failure + failures.append(f"POST {url}: {exc!r}") + else: + if problem := turnstile_probe_problem(report): + failures.append(f"POST {url}: {problem}") + print(f"POST {url} -> Turnstile secret {report.get('secret')}, mode {report.get('challenge_mode')}") + if failures: for failure in failures: print(failure, file=sys.stderr) diff --git a/src/asset_manifest.py b/src/asset_manifest.py index ffa331f..d242400 100644 --- a/src/asset_manifest.py +++ b/src/asset_manifest.py @@ -1,3 +1,3 @@ # Generated by scripts/fingerprint_assets.py. Do not edit by hand. ASSET_PATHS = {'SITE_CSS': '/site.07c59ae07f87.css', 'SYNTAX_JS': '/syntax-highlight.743e2535f133.js', 'EDITOR_JS': '/editor.3659b44c8480.js', 'RUNNER_JS': '/runner.05ddb60aa9e7.js', 'SEARCH_JS': '/search.e5a5822d9917.js', 'SEARCH_INDEX': '/search-index.332712b2e502.json'} -HTML_CACHE_VERSION = '6e28b930c962' +HTML_CACHE_VERSION = 'deed3f3ebbb1' diff --git a/src/main.py b/src/main.py index d392933..d316c5b 100644 --- a/src/main.py +++ b/src/main.py @@ -58,6 +58,11 @@ } ) SMOKE_BYPASS_HEADER = "x-pythonbyexample-smoke-secret" +TURNSTILE_PROBE_PATH = "/__smoke/turnstile" +# Cloudflare's documented dummy token. Production secrets reject it with +# invalid-input-response and a bad secret gets invalid-input-secret, so it +# proves the deployed secret without solving a challenge. +TURNSTILE_PROBE_TOKEN = "XXXX.DUMMY.TOKEN.XXXX" TURNSTILE_CLEARANCE_COOKIE = "pbe_turnstile_clearance" DEFAULT_TURNSTILE_CLEARANCE_SECONDS = 60 * 60 * 8 # Generous ceiling for an edited example program (the largest curated @@ -400,6 +405,61 @@ async def run_example(slug: str, request: Request): return response +@app.post(TURNSTILE_PROBE_PATH) +async def turnstile_probe(request: Request): + # Deployment smoke skips challenges with the bypass header, so it needs its + # own proof that the production secret works. The same header gates this. + if not _smoke_bypass_ok(request): + return _html(render_not_found(), 404) + report = await _probe_turnstile(request) + if event := _wide_event(request): + event["turnstile_probe"] = {"secret": report["secret"], "ok": report["ok"]} + return Response(json.dumps(report, sort_keys=True), media_type="application/json") + + +async def _probe_turnstile(request: Request) -> dict: + """Report whether the deployed Turnstile configuration can verify browsers.""" + site_key_configured = bool(_turnstile_site_key(request)) + report = { + "challenge_mode": _turnstile_challenge_mode(request), + "site_key_configured": site_key_configured, + } + secret = _turnstile_secret(request) + if not secret: + report["secret"] = "absent" + elif js_fetch is None or JsRequest is None: + report["secret"] = "unverified" + else: + report["secret"], error_codes = _classify_probe_reply( + await _call_siteverify(secret, TURNSTILE_PROBE_TOKEN) + ) + if error_codes and report["secret"] != "valid": + report["error_codes"] = error_codes + # No secret leaves Turnstile off by configuration. A configured secret must + # be a working production secret with a site key, or challenges cannot pass. + report["ok"] = report["secret"] == "absent" or (report["secret"] == "valid" and site_key_configured) + return report + + +def _classify_probe_reply(reply: tuple[int, dict] | None) -> tuple[str, list[str]]: + if reply is None: + return "unverified", [] + status, result = reply + error_codes = _siteverify_error_codes(result) + metadata = result.get("metadata") + # Only test secrets accept the dummy token, and the always-fail test secret + # answers exactly like a production one; this flag tells them apart. + if result.get("success") is True or ( + isinstance(metadata, dict) and metadata.get("result_with_testing_key") is True + ): + return "testing_key", error_codes + if {"invalid-input-secret", "missing-input-secret"} & set(error_codes): + return "invalid", error_codes + if 200 <= status < 300 and error_codes == ["invalid-input-response"]: + return "valid", error_codes + return "unexpected", error_codes + + def _turnstile_not_required_outcome(request: Request) -> str: if not _turnstile_secret(request) or _turnstile_challenge_mode(request) == "off": return "disabled" @@ -426,6 +486,29 @@ def _siteverify_error_codes(result: dict) -> list[str]: ) +async def _call_siteverify(secret: str, token: str, remote_ip: str | None = None) -> tuple[int, dict] | None: + """POST one token to Siteverify; None when it is unreachable or unparseable.""" + payload = {"secret": secret, "response": token} + if remote_ip: + payload["remoteip"] = remote_ip + init = { + "method": "POST", + "body": urlencode(payload), + "headers": {"Content-Type": "application/x-www-form-urlencoded"}, + } + if AbortSignal is not None: + # A hung Siteverify must not hold a learner's run or a smoke probe open. + init["signal"] = AbortSignal.timeout(SITEVERIFY_TIMEOUT_MS) + verify_request = JsRequest.new(TURNSTILE_VERIFY_URL, _to_js_object(init)) + try: + response = await js_fetch(verify_request) + status = int(getattr(response, "status", 0) or 0) + result = json.loads(await response.text()) + except Exception: # noqa: BLE001 - JS fetch failures, timeouts, and unparseable bodies fail closed + return None + return (status, result) if isinstance(result, dict) else None + + async def _verify_turnstile(request: Request, token: str) -> tuple[bool, str, dict]: """Return (ok, learner message, wide-event turnstile fields). @@ -455,27 +538,10 @@ async def _verify_turnstile(request: Request, token: str) -> tuple[bool, str, di {"outcome": "fail", "reason": "runtime_unavailable"}, ) - payload = {"secret": secret, "response": token} - remote_ip = request.headers.get("CF-Connecting-IP") - if remote_ip: - payload["remoteip"] = remote_ip - init = { - "method": "POST", - "body": urlencode(payload), - "headers": {"Content-Type": "application/x-www-form-urlencoded"}, - } - if AbortSignal is not None: - # A hung Siteverify must not hold the learner's run open indefinitely. - init["signal"] = AbortSignal.timeout(SITEVERIFY_TIMEOUT_MS) - verify_request = JsRequest.new(TURNSTILE_VERIFY_URL, _to_js_object(init)) - try: - response = await js_fetch(verify_request) - status = int(getattr(response, "status", 0) or 0) - result = json.loads(await response.text()) - except Exception: # noqa: BLE001 - JS fetch failures, timeouts, and unparseable bodies fail closed - return _turnstile_failure("siteverify_unavailable") - if not isinstance(result, dict): + reply = await _call_siteverify(secret, token, request.headers.get("CF-Connecting-IP")) + if reply is None: return _turnstile_failure("siteverify_unavailable") + status, result = reply # Siteverify answers a bad or missing secret with HTTP 400 and a JSON body # naming it, so read error codes before judging the status: discarding # non-2xx bodies would report a misconfigured deployment as an outage. diff --git a/tests/test_main_turnstile.py b/tests/test_main_turnstile.py index 2d7e0d8..8d0d385 100644 --- a/tests/test_main_turnstile.py +++ b/tests/test_main_turnstile.py @@ -8,6 +8,7 @@ from __future__ import annotations import asyncio +import json import sys import time import types @@ -495,6 +496,144 @@ def test_non_2xx_never_passes_even_when_the_body_claims_success(self): self.assertEqual(details, {"outcome": "fail", "reason": "rejected", "error_codes": ["internal-error"]}) +# Replies live Siteverify gave to the dummy token, per secret. +PRODUCTION_SECRET_REPLY = (200, '{"success": false, "error-codes": ["invalid-input-response"]}') +BAD_SECRET_REPLY = (400, '{"error-codes":["invalid-input-secret"],"success":false,"messages":[]}') +TEST_SECRET_REPLIES = { + "always-pass": ( + 200, + ( + '{"challenge_ts":"2026-09-25T22:08:06.790Z","error-codes":[],"hostname":"example.com",' + '"metadata":{"result_with_testing_key":true},"success":true}' + ), + ), + "always-fail": ( + 200, + ( + '{"error-codes":["invalid-input-response"],"success":false,"messages":[],' + '"metadata":{"result_with_testing_key":true}}' + ), + ), + "token-spent": ( + 200, + ( + '{"error-codes":["timeout-or-duplicate"],"success":false,"messages":[],' + '"metadata":{"result_with_testing_key":true}}' + ), + ), +} + + +class TurnstileProbeTests(unittest.TestCase): + """The smoke-gated probe proves the deployed secret with the dummy token. + + Response bodies are the ones live Siteverify returned for the dummy token. + """ + + def setUp(self): + self.saved = {name: getattr(main, name) for name in ("js_fetch", "JsRequest", "AbortSignal")} + main.JsRequest = type("Request", (), {"new": staticmethod(lambda url, options: (url, options))}) + main.AbortSignal = None + self.sent = [] + + def tearDown(self): + for name, value in self.saved.items(): + setattr(main, name, value) + + def _respond(self, status, body): + async def fetch(request): + self.sent.append(request) + return _SiteverifyResponse(status, body) + + main.js_fetch = fetch + + def _smoke_request(self, header="smoke-secret", **env): + values = {"PBE_SMOKE_BYPASS_SECRET": "smoke-secret", "TURNSTILE_SITE_KEY": "site-key"} + values.update(env) + headers = {main.SMOKE_BYPASS_HEADER: header} if header else {} + return make_request(headers=headers, env=session_env(**values)) + + def _probe(self, request): + return asyncio.run(main._probe_turnstile(request)) + + def test_working_production_secret_is_ok(self): + self._respond(*PRODUCTION_SECRET_REPLY) + report = self._probe(self._smoke_request()) + self.assertEqual( + report, + {"challenge_mode": "session", "site_key_configured": True, "secret": "valid", "ok": True}, + ) + _, options = self.sent[0] + self.assertIn(f"response={main.TURNSTILE_PROBE_TOKEN}", options["body"]) + self.assertIn("secret=server-secret", options["body"]) + self.assertNotIn("remoteip", options["body"]) + + def test_bad_secret_fails_with_its_error_code(self): + self._respond(*BAD_SECRET_REPLY) + report = self._probe(self._smoke_request()) + self.assertEqual(report["secret"], "invalid") + self.assertEqual(report["error_codes"], ["invalid-input-secret"]) + self.assertFalse(report["ok"]) + + def test_every_test_secret_fails_including_the_one_that_mimics_production(self): + for name, response in TEST_SECRET_REPLIES.items(): + with self.subTest(secret=name): + self._respond(*response) + report = self._probe(self._smoke_request()) + self.assertEqual(report["secret"], "testing_key") + self.assertFalse(report["ok"]) + + def test_valid_secret_without_site_key_is_not_ok(self): + self._respond(*PRODUCTION_SECRET_REPLY) + report = self._probe(self._smoke_request(TURNSTILE_SITE_KEY="")) + self.assertEqual(report["secret"], "valid") + self.assertFalse(report["site_key_configured"]) + self.assertFalse(report["ok"]) + + def test_absent_secret_reports_turnstile_off_without_calling_siteverify(self): + self._respond(*PRODUCTION_SECRET_REPLY) + report = self._probe(self._smoke_request(TURNSTILE_SECRET_KEY="")) + self.assertEqual(report["secret"], "absent") + self.assertTrue(report["ok"]) + self.assertEqual(self.sent, []) + + def test_unreachable_or_unexpected_siteverify_is_not_ok(self): + async def failing(_request): + raise RuntimeError("network down") + + main.js_fetch = failing + self.assertEqual(self._probe(self._smoke_request())["secret"], "unverified") + for status, body in ( + (200, '{"success": false, "error-codes": ["internal-error"]}'), + (200, '{"success": false}'), + (503, '{"success": false, "error-codes": ["invalid-input-response"]}'), + ): + with self.subTest(status=status, body=body): + self._respond(status, body) + report = self._probe(self._smoke_request()) + self.assertEqual(report["secret"], "unexpected") + self.assertFalse(report["ok"]) + + def test_route_requires_the_smoke_header_and_returns_json(self): + self._respond(*BAD_SECRET_REPLY) + for header in ("", "wrong-secret"): + with self.subTest(header=header): + response = asyncio.run(main.turnstile_probe(self._smoke_request(header=header))) + self.assertEqual(response.status_code, 404) + self.assertEqual(self.sent, [], "an unauthenticated probe must not reach Siteverify") + + request = self._smoke_request() + request.state.wide_event = {"path": main.TURNSTILE_PROBE_PATH} + response = asyncio.run(main.turnstile_probe(request)) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.media_type, "application/json") + report = json.loads(response.body) + self.assertEqual(report["secret"], "invalid") + self.assertFalse(report["ok"]) + self.assertEqual(request.state.wide_event["turnstile_probe"], {"secret": "invalid", "ok": False}) + self.assertNotIn("server-secret", response.body.decode()) + + async def _awaitable(value): return value diff --git a/tests/test_smoke_deployment.py b/tests/test_smoke_deployment.py index 9600d37..cb49a02 100644 --- a/tests/test_smoke_deployment.py +++ b/tests/test_smoke_deployment.py @@ -1,6 +1,12 @@ +import contextlib import importlib.util +import io +import os import pathlib +import sys import unittest +import urllib.error +from unittest import mock ROOT = pathlib.Path(__file__).resolve().parents[1] @@ -41,5 +47,81 @@ def test_smoke_bypass_secret_requires_https_origin(self): ) +OK_PROBE_REPORT = {"challenge_mode": "session", "ok": True, "secret": "valid", "site_key_configured": True} + + +class TurnstileProbeSmokeTests(unittest.TestCase): + """Bypass-authenticated smoke must still prove the deployed Turnstile secret.""" + + def setUp(self): + self.smoke = load_smoke_module() + self.probes = [] + + def run_smoke(self, report=None, *, secret="smoke-secret", probe_error=None): + smoke = self.smoke + + def fake_probe(base_url, smoke_bypass_secret): + self.probes.append((base_url, smoke_bypass_secret)) + if probe_error is not None: + raise probe_error + return report + + def fake_post(url, code, smoke_bypass_secret=""): + expected = next(marker for _, source, marker in smoke.POST_SMOKES if source == code) + return 200, f'
{expected}
' + + stdout, stderr = io.StringIO(), io.StringIO() + env = {"PBE_SMOKE_BYPASS_SECRET": secret} if secret else {} + with ( + mock.patch.object(smoke, "fetch", lambda url: (200, "ok")), + mock.patch.object(smoke, "post_code", fake_post), + mock.patch.object(smoke, "probe_turnstile", fake_probe), + mock.patch.object(sys, "argv", ["smoke_deployment.py", "https://www.pythonbyexample.dev"]), + mock.patch.dict(os.environ, env, clear=True), + contextlib.redirect_stdout(stdout), + contextlib.redirect_stderr(stderr), + ): + code = smoke.main() + return code, stdout.getvalue(), stderr.getvalue() + + def test_working_secret_passes_and_is_reported(self): + code, stdout, _ = self.run_smoke(OK_PROBE_REPORT) + self.assertEqual(code, 0) + self.assertEqual(self.probes, [("https://www.pythonbyexample.dev/", "smoke-secret")]) + self.assertIn("Turnstile secret valid, mode session", stdout) + self.assertIn("Deployment smoke OK", stdout) + + def test_bad_secret_fails_smoke_even_though_bypassed_runs_pass(self): + report = {**OK_PROBE_REPORT, "ok": False, "secret": "invalid", "error_codes": ["invalid-input-secret"]} + code, _, stderr = self.run_smoke(report) + self.assertEqual(code, 1) + self.assertIn("Siteverify rejects TURNSTILE_SECRET_KEY", stderr) + self.assertIn("invalid-input-secret", stderr) + + def test_missing_probe_route_or_wrong_bypass_secret_fails(self): + error = urllib.error.HTTPError("https://www.pythonbyexample.dev/__smoke/turnstile", 404, "Not Found", {}, None) + code, _, stderr = self.run_smoke(probe_error=error) + self.assertEqual(code, 1) + self.assertIn("HTTP 404", stderr) + + def test_without_bypass_secret_the_probe_is_skipped_not_failed(self): + code, stdout, _ = self.run_smoke(secret="") + self.assertEqual(self.probes, []) + self.assertIn("SKIP", stdout) + self.assertEqual(code, 0) + + def test_probe_problems_name_the_misconfiguration(self): + problem = self.smoke.turnstile_probe_problem + self.assertIsNone(problem(OK_PROBE_REPORT)) + self.assertIsNone(problem({"ok": True, "secret": "absent", "site_key_configured": False})) + self.assertIn("test secret", problem({"ok": False, "secret": "testing_key"})) + self.assertIn( + "TURNSTILE_SITE_KEY", problem({"ok": False, "secret": "valid", "site_key_configured": False}) + ) + self.assertIn("could not reach Siteverify", problem({"ok": False, "secret": "unverified"})) + self.assertIn("internal-error", problem({"ok": False, "secret": "unexpected", "error_codes": ["internal-error"]})) + self.assertIn("unrecognized", problem({"ok": False, "secret": "new-state"})) + + if __name__ == "__main__": unittest.main() From 544395ab219f2dd7cb85395c32c7136d53b259dd Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 09:46:03 +0000 Subject: [PATCH 4/4] Add visual evidence for the Turnstile rejection loop Capture /examples/values after one Run click on origin/main and on this branch. Both Workers used Cloudflare's invisible always-pass test site key, so the real widget solved in the browser, and the always-fail test secret, so Siteverify rejected every token. main kept re-challenging (12 POSTs and 11 solves in 20 seconds); the branch stops after one solve with the server's message. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PTpdP6DbuVU4VPLRzjNYGu --- docs/pr-evidence/README.md | 19 ++++++++++++++++++ .../turnstile-rejection-after-runner.png | Bin 0 -> 57007 bytes .../turnstile-rejection-before-runner.png | Bin 0 -> 54768 bytes 3 files changed, 19 insertions(+) create mode 100644 docs/pr-evidence/turnstile-rejection-after-runner.png create mode 100644 docs/pr-evidence/turnstile-rejection-before-runner.png diff --git a/docs/pr-evidence/README.md b/docs/pr-evidence/README.md index 1d504f1..bbbee45 100644 --- a/docs/pr-evidence/README.md +++ b/docs/pr-evidence/README.md @@ -1,5 +1,24 @@ # PR visual evidence +## Turnstile rejection loop (2026-09-26) + +These captures show `/examples/values` six seconds after one Run click, with Siteverify rejecting every token. Both Workers used Cloudflare's invisible always-pass test site key (`1x00000000000000000000BB`), so the real Turnstile widget loaded and solved in the browser, and the always-fail test secret (`2x0000000000000000000000000000000AA`), so Siteverify rejected every solved token. + +| Evidence | Review point | Commit | SHA-256 | +| --- | --- | --- | --- | +| [Before](turnstile-rejection-before-runner.png) | The runner re-challenges after each rejection and stays busy on "Verification required…". Over 20 seconds, one click made 12 POSTs and 11 real widget solves, growing steadily. | `3f300af` (`origin/main`) | `5af0c28f339e6bc4d394186530c385e663046a7edf98671965d0a61ed1a3cdf3` | +| [After](turnstile-rejection-after-runner.png) | One solve, two POSTs, then the server's message and a free Run button. Still two POSTs after 20 seconds. | `eae0dc3` | `20ca27224fff2f0413004b8e2e2f35e7b1563b45f3941714a68f3e46e837f0af` | + +Reproduce each capture by serving the corresponding revision with the test keys: + +```bash +uv run --group workers pywrangler dev --port \ + --var TURNSTILE_SECRET_KEY:2x0000000000000000000000000000000AA \ + --var TURNSTILE_SITE_KEY:1x00000000000000000000BB +``` + +Open `http://127.0.0.1:/examples/values` at a 1200×900 viewport, click Run once, wait six seconds, and capture `.runner-grid`. Count `POST /examples/values` requests in DevTools to see the loop. `make browser-layout-test` covers the same contract with a stub server that always rejects. + ## Audit remediation (2026-07-10) These captures isolate the dark-mode Run-button contrast correction on the same `/examples/values` runner at a 1200×900 desktop viewport. diff --git a/docs/pr-evidence/turnstile-rejection-after-runner.png b/docs/pr-evidence/turnstile-rejection-after-runner.png new file mode 100644 index 0000000000000000000000000000000000000000..d183ebbe7b4d4d67475987a1d4959858a61a0c86 GIT binary patch literal 57007 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