• Mahmoud Aglan's avatar
    fix(security): remove the mobile API surface and stop the 500 page leaking sessions · 883391c7
    Mahmoud Aglan authored
    Two live disclosures and one latent account takeover, plus the infrastructure
    defects that hid them.
    
    AuthOtpController::verify() accepted a constant '0000' whenever auth_otp_mode was
    'demo' — the value every instance was seeded with — and then minted a Sanctum
    token for whichever active user matched the submitted phone number, staff
    included. It was not exploitable as written, because 2026_08_30_000004 had
    normalised users.phone to digits-only local form while normalizePhone() produced
    +20…, so the lookup missed. That is one plausible bug-fix away from being live,
    which is why the whole surface goes rather than the branch.
    
    Deleting /api/v1 also removes: broadcast/send pushing to every device in the
    academy with no permission check; ReceiptController's inverted ownership check,
    which made any non-participant invoice world-readable to any token;
    PaymentController::initiate with no ownership check at all; and
    DeviceController keying updateOrCreate on the FCM token alone, letting one user
    claim another's device. None of it is replaced — the member-facing surface is the
    session-authenticated web portal, so a second token-authenticated surface meant
    building and authorizing everything twice.
    
    bootstrap/app.php built a full diagnostic payload for any 500 and errors/500
    rendered it to the browser, ungated by APP_DEBUG. The session it printed carries
    password_hash_web — the signed-in user's bcrypt hash — alongside the last ten
    queries, the request input and the headers. Now gated on debug, auth keys
    stripped by prefix even there, and the production page is self-contained with no
    CDN. Detail still reaches storage/logs, keyed by the error id shown to the user.
    
    ParentHome::$activeChildId was validated in mount() and selectChild() but used
    raw in render() at eight query sites. Livewire is ^4.3, where a public property
    is settable from the browser, so those checks were decoration: a guardian could
    walk participant ids and read any child's balance, attendance and evaluations.
    Locked, and re-validated in render() since the child list can change between
    requests.
    
    ParentExcuseForm wrote the attachment — typically a child's medical note — to the
    PUBLIC disk, then discarded the record and flashed success. The parent believed
    the absence was excused; nothing was stored, and the record kept feeding the
    consecutive-absence threshold that auto-suspends a participant. It now stores
    nothing and says so, until excuses are modelled properly.
    
    Infrastructure, because each one hid a failure rather than causing one:
    entrypoint continued booting after a failed migration, which serves a stale
    schema and silently blocks every later migration forever; the env whitelist had
    no PAYMOB_, so config:cache baked null credentials and the gateway failed closed
    with no error anywhere; nginx's static-asset regex answered =404 for /sw.js
    before PHP saw it; and Route::fallback returned 200 for every unrouted path, so
    a deleted endpoint served a website page instead of 404.
    
    Verified: 43/44 tests pass. The one failure is ExampleTest, which fails
    identically on unmodified main — confirmed by stashing. Two new tests pin both
    disclosures so they cannot return.
    Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
    883391c7
Name
Last commit
Last update
..
Blocks Loading commit data...
Enums Loading commit data...
Models Loading commit data...
Services Loading commit data...