• 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
..
CheckPermission.php Loading commit data...
EnsureSuperAdmin.php Loading commit data...
LogUserActivity.php Loading commit data...
RequireBranchSelection.php Loading commit data...
ResolveAcademyFromSlug.php Loading commit data...
ResolveBranchContext.php Loading commit data...
SetCurrentAcademy.php Loading commit data...
SetLocale.php Loading commit data...