← All repos

alist-portal

Laravel 11 monolith powering the entire A-List platform: admin/operations panel, vendor and creator portals, and the mobile-app REST API.

Primary stackPHP 8.2 / Laravel 11 Last commit2026-05-08 Repo size526 MB Files2,589 Branchmaster Health critical

Executive summary

Tech stack

PHP 8.2 Laravel 11.48 MySQL Redis (AWS ElastiCache) Passport OAuth2 Spatie Permission 6 Pusher Echo + Socket.IO Stripe 10 Sentry 4 Firebase FCM S3 (flysystem 3) Blade + Vue 2 + jQuery Laravel Mix 6
CategoryTechnologyVersionNotes
LanguagePHP^8.2composer.json — but .htaccess still pins ea-php73 handler. Verify live runtime.
FrameworkLaravelv11.48.0Recent. Migrated up from older Laravel (legacy patterns linger).
Web authSession + Spatie Permission6.24.1Custom PermissionMiddleware with hardcoded route exceptions.
API authLaravel Passportv13.4.4OAuth2 against signups provider (creator users).
DBMySQL via Eloquent + doctrine/dbal3.x381 migrations. Hardcoded TZ Asia/Dubai.
Cache/Queue/BroadcastRedispredis 1.1.10AWS ElastiCache host leaked in laravel-echo-server.json.
RealtimePusher + laravel-echo-server (Redis) + bespoke server.js socket.ioEcho on :6001, custom socket on :8081 (CORS origin: "*").
Paymentsstripe/stripe-phpv10.21.0Subscriptions + webhooks via SubscriptionController.
MailLaravel Mail + jdavidbakr/mail-tracker7.xTracked opens/clicks; mail-tracker stores all sent mail in DB.
Error trackingsentry/sentry-laravel4.20.1Wired in config/app.php providers.
PDF / Excelbarryvdh/laravel-dompdf 3.0.0, maatwebsite/excel 3.1.58Review-archive PDFs + admin exports.
Imagesintervention/image 2.7.2, league/flysystem-aws-s3-v3 3.0S3 uploads.
3rd-partyGoogle APIs, FCM, Modash, TikTok, Interakt (WhatsApp), Stevebauman geo-IPMany of these have credentials currently committed.
FrontendBlade + jQuery + Bootstrap 4 + Vue 2.5Laravel Mix 6Mostly server-rendered admin pages.
TestsPHPUnit11.xOnly stub ExampleTest.php files — effectively zero coverage.

Architecture

Standard Laravel directory layout — but Eloquent models live directly under app/ (not app/Models/), Composer autoloads nine global helper files, and most business logic is split between very large controllers and an even larger "repository" layer registered through App\Providers\RepositoryServiceProvider. Five middleware groups (web, admin, chat_agent, creator, business) front separate route files. Queues + broadcasts + cache all go through Redis; the scheduler queries the DB on every tick to decide whether to register Modash/TikTok cron jobs.

alist-portal/ ├── app/ │ ├── *.php ~100 Eloquent models at the top level (Signup, Merchant, Venues, FoodOffers, …) │ ├── Console/ │ │ ├── Kernel.php cron schedule — reads DB at boot │ │ └── Commands/ 22 artisan commands (offer publish, newsletter, review archive…) │ ├── Events / Listeners / Notifications / Observers / Mail / Exports │ ├── Helper/ 9 globally autoloaded procedural helpers (helpers, settings, pushNotification, …) │ ├── Http/ │ │ ├── Kernel.php middleware groups │ │ ├── Middleware/ Cors, AdminAuthValidate, PermissionMiddleware, TokenAuthentication, DebugerMiddleware… │ │ └── Controllers/ │ │ ├── Admin/ 28 admin controllers, AdminController.php = 5876 LOC │ │ ├── Api/v1/ mobile REST endpoints │ │ ├── Auth/ Laravel auth scaffolding + Instagram OAuth │ │ └── ChatAgent/ chat-agent sub-portal │ ├── Jobs/ ~45 queued jobs (push, mailers, broadcasts, analytics fetch, archive…) │ ├── Repositories/ "repository pattern" — OfferRepository.php = 9770 LOC │ ├── Services/ FCMService, WhatsAppService, GoogleContactsService, YoutubeAuthService │ └── Providers/ AppServiceProvider, RouteServiceProvider, RepositoryServiceProvider, QueueRateLimitServiceProvider, … ├── routes/ admin.php / api.php / api_v1.php / web.php / business.php / creator.php / chat_agent.php / channels.php / console.php ├── config/ 29 config files (auth, cors, queue, permission, sentry, websockets, …) ├── database/migrations/ 381 migrations (2017 → 2026-04-29) ├── resources/views/ Blade admin/creator/merchant templates ├── public/ document root via .htaccess rewrite; mixes static + compiled assets ├── storage/ fcm.json, googlePeople.json (both committed with live credentials!) ├── server.js bespoke socket.io daemon ├── laravel-echo-server.json socket.io broker config (leaks AWS Redis host) ├── .config.json ⚠ committed Google OAuth client_secret + refresh token ├── .info.php ⚠ committed phpinfo() page ├── token.txt ⚠ committed plaintext token └── error_log ⚠ 758 KB committed error log with internal server paths

Security assessment

This is a white-box source-code security review of the entire alist-portal monolith — admin panel, creator/vendor portals, and the mobile v1 REST API. Every finding below was confirmed by reading the cited source (routes, middleware, controllers, repositories, config, and git history); no finding is speculative. Severity is assigned with CVSS 3.1 base scoring and mapped to OWASP Top 10 2021, the OWASP API Security Top 10 2023, OWASP MASVS (this is a mobile-app backend), and CWE; risk is framed against NIST CSF 2.0 functions.

OWASP Top 10 2021 OWASP WSTG OWASP API Top 10 2023 OWASP MASVS (mobile) CWE CVSS 3.1 NIST CSF 2.0
Anthropic cybersecurity playbooks applied to this repo: performing-web-application-penetration-test performing-web-application-vulnerability-triage testing-for-broken-access-control bypassing-authentication-with-forced-browsing testing-api-security-with-owasp-top-10 conducting-api-security-testing testing-api-authentication-weaknesses testing-mobile-api-authentication exploiting-sql-injection-vulnerabilities performing-second-order-sql-injection exploiting-server-side-request-forgery performing-blind-ssrf-exploitation implementing-secret-scanning-with-gitleaks detecting-aws-credential-exposure-with-trufflehog performing-service-account-credential-rotation implementing-secrets-scanning-in-ci-cd

Risk narrative

alist-portal is the single Laravel monolith behind the entire A-List platform (admin/ops panel, vendor and creator portals, and the mobile REST API), and it is in a critical state across nearly every OWASP category. Multiple production secrets are committed and actively loaded at runtime — a Firebase Admin RSA private key, two sets of Google OAuth client_secret+refresh tokens, live Slack/Monday credentials, hard-coded DB and Stripe keys — meaning anyone who has ever cloned the Bitbucket repo can impersonate backend services. Authentication is fundamentally broken: a 4-digit OTP login with no lockout, the OTP echoed back in the API response, an unauthenticated IDOR that discloses any user's verification PIN, and a permanent backdoor account, together enabling trivial unauthenticated account takeover of the whole creator base. Access control collapses to "authenticated == full admin" because the custom permission middleware bypasses all checks for AJAX requests, while ~39 destructive admin routes and a block of creator routes sit entirely outside any auth gate. Layered on top are several confirmed SQL injections in admin endpoints, a wildcard CORS header on every response, server-side fetch of attacker URLs (SSRF to AWS IMDS/Redis), broken payment/webhook logic, and non-atomic voucher redemption enabling merchant-funded fraud — all running on an end-of-life PHP 7.3 interpreter with no CI or dependency scanning.

Overall risk
CRITICAL
7 P0 18 P1 11 P2 11 P3

OWASP coverage

CategoryStatusNotes
A01:2021 — Broken Access ControlvulnAJAX bypasses all permission checks (AP-02); ~39 admin routes + creator routes outside auth groups (AP-03, AP-16); IDOR on notifications/reviews/venue (AP-17, AP-35); CSRF and open redirect (AP-33, AP-34).
A02:2021 — Cryptographic FailuresvulnFirebase SA key, Google OAuth secrets+refresh tokens, DB password, RSA pem, Stripe/whsec secrets committed (AP-01, AP-07, AP-08, AP-26, AP-28); plaintext PIN/OTP storage (AP-22); weak SHA-1/MD5 and mt_rand (AP-38, AP-43).
A03:2021 — InjectionvulnFour confirmed admin SQLi via DB::select string concatenation (AP-09 to AP-13) plus second-order SQLi via stored avail_days on the mobile API hot path (AP-14).
A04:2021 — Insecure DesignvulnNon-atomic voucher redemption / TOCTOU double-spend (AP-25), broken Stripe webhook/payment flow (AP-31), client-trusted redemption ids/flags (AP-32), brute-forceable static venue PIN (AP-24), over-broad Signup $fillable (AP-41).
A05:2021 — Security MisconfigurationvulnWildcard CORS on every response (AP-18), TrustProxies '*' (AP-30), web-served phpinfo/error_log/OAuth-script (AP-29), missing security headers (AP-45), echo-server/socket.io wildcard CORS + leaked Redis (AP-37), insecure cookie flags (AP-44).
A06:2021 — Vulnerable and Outdated ComponentsvulnEOL PHP 7.3 runtime (AP-20), EOL intervention/image 2.7.2 on unauthenticated routes (AP-36), EOL PHP deps (AP-47) and frontend stack (AP-48), no CI/SCA gate (AP-49).
A07:2021 — Identification and Authentication Failuresvuln4-digit OTP with no lockout (AP-04), OTP echoed in response (AP-05), unauthenticated PIN-disclosure IDOR (AP-06), hardcoded backdoor OTP (AP-21), long-lived non-revocable Passport tokens (AP-23).
A08:2021 — Software and Data Integrity FailurespartialNo build/deploy integrity (manual FTP, no SBOM/lockfile verification — see AP-49); no insecure-deserialization sink confirmed in app code.
A09:2021 — Security Logging and Monitoring FailuresvulnOAuth tokens logged at INFO (AP-40) and a 774 KB error_log committed/web-served leaking paths and account names (AP-39); no centralized monitoring/alerting observed.
A10:2021 — Server-Side Request Forgery (SSRF)vulnfile_get_contents on user-supplied image URLs reaching IMDS/internal Redis/file:// (AP-19), reachable from low-privilege creator and signup flows.
API Top 10 (2023)vulnAPI1 BOLA (AP-15, AP-17, AP-32, AP-35), API2 Broken Auth (AP-04, AP-05, AP-21, AP-23), API6 sensitive business flows (redemption fraud AP-24/AP-25); the legacy /api/* file has no auth at all.
Mobile M1-M10 (server-side relevant)vulnThis is the mobile app's backend: M3 insecure auth/authz (OTP weaknesses, IDOR), M4 insufficient input/output validation (SQLi, SSRF), M9 insecure data storage of secrets (committed keys), M10 insufficient cryptography (plaintext PIN/OTP, mt_rand) all apply server-side.

Detailed findings

P0 — Critical (7) P1 — High (18) P2 — Medium (11) P3 — Low / Info (11)
AP-01

Firebase Admin service-account RSA private key committed in storage/fcm.json and loaded at runtime

P0 A02:2021 - Cryptographic Failures CWE-798: Use of Hard-coded Credentials CVSS 9.8 EPSS n/a (credential leak, not a CVE) confirmed
Description

The full RSA-2048 private key for the Firebase Admin SDK service account (project alist-application) is committed in cleartext at storage/fcm.json, is git-tracked, and is loaded at runtime by app/Helper/pushNotification.php via setAuthConfig() to mint FCM access tokens. A Firebase Admin service-account key grants administrative control of the Firebase project.

Evidence
storage/fcm.json:5 contains a full RSA private key block (-----BEGIN PRIVATE KEY----- ... -----END PRIVATE KEY-----) for service account firebase-adminsdk-s5r6t@alist-application.iam.gserviceaccount.com, project_id alist-application, private_key_id d07b7e1be998da23d756303beeb0bf7bbdb96e88 config/fcm.php returns ['credentials' => 'fcm.json'] app/Helper/pushNotification.php:37 $client->setAuthConfig(storage_path(config('fcm.credentials'))); then fetchAccessTokenWithAssertion() with scope firebase.messaging — the committed key authenticates production FCM calls git ls-files lists storage/fcm.json; git log shows commit 812e6f574 'fcm creds'
Attack scenario
An attacker (or ex-employee with a clone) extracts the private key, signs a Google service-account JWT to obtain an FCM access token, then POSTs to the FCM v1 send endpoint to blast a malicious 'account locked, tap to verify' push to all users, and if other Firebase services are enabled, reads/rewrites data and forges user identity tokens.
Impact

Anyone with read access to the repo or any clone can impersonate the A-List backend to Firebase: send arbitrary push to the entire mobile user base, and (if Firestore/RTDB/Storage are enabled) read/write backend data and mint custom auth tokens to impersonate any user. Full compromise of the Firebase trust boundary.

Remediation
Immediately delete this service-account key in Google Cloud IAM (key id d07b7e1be998da23d756303beeb0bf7bbdb96e88) and issue a new one. Remove storage/fcm.json from the tree and purge from history (git filter-repo/BFG). Mount the new key from outside the repo / a secret store and gitignore the path. Add gitleaks/trufflehog pre-commit + CI gates. Audit Firebase audit logs for abuse since first commit.
Verifier note

Confirmed by reading storage/fcm.json, config/fcm.php, and pushNotification.php:37; key is verbatim and runtime-loaded.

AP-02

AJAX requests bypass ALL admin permission checks in PermissionMiddleware (vertical privilege escalation)

P0 A01:2021 - Broken Access Control CWE-863: Incorrect Authorization CVSS 8.8 confirmed
Description

The custom Spatie wrapper PermissionMiddleware short-circuits authorization for any request flagged as AJAX. Because X-Requested-With is client-supplied, any authenticated admin-panel principal (low-privilege role, chat agent, single-permission account) can invoke ANY permission-protected admin route by sending it as AJAX, collapsing the RBAC model to 'authenticated == full admin' for the majority of the panel.

Evidence
app/Http/Middleware/PermissionMiddleware.php:110-117 — inside the per-permission loop: if ($request->ajax()) { return $next($request); } else { if ($authGuard->user()->can($permission)) { return $next($request); } } — ->can() is only evaluated in the else branch, so any AJAX request is admitted on the first loop iteration without any permission check Laravel $request->ajax() returns true whenever the client sends X-Requested-With: XMLHttpRequest routes/admin.php:50 wraps the bulk of the back-office in Route::group(['middleware' => ['auth','permission']]); most admin mutations are POST AJAX Only role==1 super-admins (line 81) and zero-permission users (line 87) are handled separately; any authenticated user with >=1 permission or a chat agent reaches every permission-gated action via AJAX
Attack scenario
A chat-agent account logs into admin.alist.ae, then issues GET/POST to a high-privilege endpoint (e.g. user PII export or decline_signup) with header X-Requested-With: XMLHttpRequest; the middleware admits it without checking the permission, granting full admin capability.
Impact

Complete vertical privilege escalation within the admin/operations console. A minimally-privileged staff or chat-agent account can perform any administrative action: approve/decline creators, bulk PII export, user/role management, offer/venue/merchant management.

Remediation
Remove the if ($request->ajax()) return $next($request); bypass entirely; evaluate ->can($permission) identically for AJAX and non-AJAX. Prefer Laravel's built-in can/policy/gate middleware. Add tests asserting a permission-less user gets 403 on every named admin route regardless of headers.
Verifier note

Confirmed at PermissionMiddleware.php:110-117; the AJAX branch is the only path that skips ->can().

AP-03

~39 admin routes defined OUTSIDE the auth/permission group are reachable unauthenticated (incl. destructive deletes and PII)

P0 A01:2021 - Broken Access Control CWE-862: Missing Authorization CVSS 9.1 confirmed
Description

A block of admin GET/POST routes was appended after the closing brace of the ['auth','permission'] group. The file-level admin middleware group has no authentication, so these ~39 routes execute for unauthenticated callers on the admin domain. They include destructive offer-code mutations, campaign data exports, and a creator-PII viewer; the destructive handlers write before any Auth reference.

Evidence
routes/admin.php:792 closes the Route::group(['middleware' => ['auth','permission']]) block; routes/admin.php:794-832 declare ~39 routes AFTER it, including instagram-insight/{id}, getFoodOfferDateUsers, offer-code/{id}/download-click-logs, campaign/{id}/click-report, resetUserDate, moveOfferCodeDate, deleteOfferCodeDate, deleteofferdate, savenewofferdatetocampaign/{id}, auth/google/callback app/Providers/RouteServiceProvider.php:196-200 mapAdminRoutes applies only ['admin','throttle:admin']; the admin group in app/Http/Kernel.php:46-55 contains EncryptCookies/StartSession/CSRF/SubstituteBindings/Debuger — NO auth, NO permission app/Http/Controllers/Admin/FoodOffersDatesController.php:1375-1381 deleteOfferCode() runs FoodOffersUsers::where('id',$offer_user_id)->delete() using request input BEFORE any Auth::user() reference (Auth is only touched later inside the if($FoodOfferDates) branch at ~1402) app/Http/Controllers/SignupController.php:783-808 instagramInsightView($id) returns creator name, follower range and Instagram insight screenshot URLs with no auth — only Hashids obfuscation of the integer id
Attack scenario
An attacker GETs https://admin.alist.ae/instagram-insight/<hashid> to harvest creator PII, then POSTs to /deleteOfferCodeDate or /resetUserDate with a guessed offer_user_id to delete redemption rows — all without a session, because deleteOfferCode deletes before touching Auth.
Impact

Unauthenticated attackers can read creator PII (names, follower tiers, Instagram insight screenshots), export per-offer/campaign click analytics, and destructively delete/reset offer-code and redemption rows, corrupting live campaign state and creator redemptions. IDs are sequential integers or trivially-reversible Hashids, enabling enumeration.

Remediation
Move all routes at routes/admin.php:794-832 inside the ['auth','permission'] group, or better attach auth (and a permission/policy) at the route-group/file level in RouteServiceProvider::mapAdminRoutes so a route can never silently escape authentication. Verify destructive handlers re-check authorization before acting.
Verifier note

Confirmed: routes/admin.php:792 closes the group; 794-832 are outside; admin group in Kernel has no auth; deleteOfferCode deletes before Auth at line 1381.

AP-04

Brute-forceable 4-digit OTP login with no per-account lockout (unauthenticated account takeover)

P0 API2:2023 Broken Authentication / A07:2021 Identification and Authentication Failures CWE-307: Improper Restriction of Excessive Authentication Attempts CVSS 9.1 confirmed
Description

Both the mobile API (/v1/login/verify-otp[v2]) and creator web portal authenticate by matching a 4-digit numeric OTP (9000 values) against a user identifier, with no failed-attempt counter, no per-account lockout, no CAPTCHA, and no OTP-specific rate limit. The only control is the framework-wide throttle:500,1, which is trivially defeated. login/with-otp returns the target user_id, so an attacker who knows a victim's email requests an OTP and iterates all 9000 values to obtain a Passport token.

Evidence
app/Repositories/SignupRepository.php:2255 loginWithOTPVerify(): Signup::withTrashed()->where([['otp',$requestData['otp']],['id',$requestData['user_id']]])->first() — matches a 4-digit OTP against a caller-supplied user_id with NO attempt counter; on match it deletes existing tokens and mints a Passport access token (~2288) app/Repositories/SignupRepository.php:900-902 generateNDigitRandomNumber(4) => mt_rand(1000,9999) — only 9000 values routes/api_v1.php:65-67 login/with-otp, login/verify-otp, login/verify-otpv2 carry NO throttle middleware (only inside the cors,json.response group) app/Http/Kernel.php:85-86 the api group applies only throttle:500,1 (500 req/min) — not an OTP lockout; the 9000-value keyspace is exhaustible in ~18 minutes per account No RateLimiter/lockout/attempt counter exists in SignupRepository or CreatorController for OTP verification
Attack scenario
Attacker submits the victim's email to /v1/login/with-otp, receives the user_id in the response, then scripts up to 9000 POSTs to /v1/login/verify-otp iterating otp 1000-9999 against that fixed user_id; on the matching value the API returns a Passport access_token and the account is taken over.
Impact

Full account takeover of any creator by an unauthenticated attacker who knows the victim's email. On a valid OTP the server mints a Passport access token, granting access to PII (email, phone, birthday, nationality), offer/redemption history, and the ability to act as the victim.

Remediation
Add per-account and per-IP failed-OTP counters with exponential backoff and hard lockout after ~5 attempts; invalidate the OTP and force re-request. Increase OTP entropy to >=6 digits via random_int(). Apply a strict route-level throttle (e.g. throttle:5,1 keyed on user_id+IP) to all OTP login routes. Bind OTP to the requesting device/session, enforce single-use and short TTL at verify time.
Verifier note

Confirmed: SignupRepository.php:2255 lookup, mt_rand 9000-keyspace, login routes have no throttle override; only global throttle:500,1.

AP-05

Login OTP returned in the API response body, defeating the email second factor

P0 API2:2023 Broken Authentication CWE-200: Exposure of Sensitive Information CVSS 9.1 confirmed
Description

The unauthenticated /v1/login/with-otp endpoint generates a login OTP, emails it, AND echoes the same plaintext OTP back in the JSON response (the developer comment 'TODO: remote OTP field after development' confirms shipped debug code). Any party who submits a victim's email receives that victim's OTP and submits it to verify-otp to obtain a Passport token, bypassing the email-possession factor entirely.

Evidence
app/Repositories/SignupRepository.php:2233-2239 loginWithOTP() returns ['status'=>true,'message'=>'OTP has been sent...','data'=>[...], // TODO: remote OTP field after development 'otp' => $otp] Reached via routes/api_v1.php:65 Route::post('login/with-otp', ...) — unauthenticated, only 'email' required app/Http/Controllers/Api/v1/SignupController.php loginWithOTP passes requestData straight to the repository whose response includes the plaintext otp
Attack scenario
Attacker POSTs {email: victim@x.com} to /v1/login/with-otp, reads otp directly from the JSON response, then POSTs {user_id, otp} to /v1/login/verify-otp and receives a Passport access token for the victim.
Impact

Trivial unauthenticated account takeover of any creator account given only the victim's email. The OTP-to-email channel the design depends on is fully circumvented, and brute force is unnecessary.

Remediation
Remove the 'otp' field from the loginWithOTP response immediately and audit all auth responses (loginWithOTP, creatorLogin, signup verify) for leaked otp/pin/randomPIN fields. Add a regression test asserting auth responses contain no otp/pin fields.
Verifier note

Confirmed at SignupRepository.php:2236 — 'otp' => $otp returned with TODO comment.

AP-06

Unauthenticated verification-PIN disclosure via IDOR (creator/otpshow/{user_id})

P0 API1:2023 Broken Object Level Authorization / A01:2021 Broken Access Control CWE-639: Authorization Bypass Through User-Controlled Key CVSS 8.6 confirmed
Description

GET creator/otpshow/{user_id}/{socialMedia} (and the adjacent linkshow route) are registered outside the authenticateToken:creator group and run under the unauthenticated 'creator' group. The handler looks up an arbitrary user by path parameter and renders a view containing that user's authenticationRandomNumber — the Instagram/TikTok ownership-verification PIN. An attacker enumerates sequential user_id values and reads every user's verification PIN without logging in.

Evidence
routes/creator.php:35 Route::get('creator/otpshow/{user_id}/{socialMedia}', 'CreatorController@otpShow') — declared BEFORE the authenticateToken:creator group (routes/creator.php:91) app/Http/Kernel.php:71-80 the 'creator' middleware group contains only EncryptCookies/StartSession/CSRF/Language — no authentication app/Http/Controllers/CreatorController.php:335-348 otpShow(): $signup = Signup::find($user_id); $otp = $signup->authenticationRandomNumber; return view('creator.login_otp_show', compact('otp','socialMedia')); — returns the verification OTP for any user_id with no ownership check (tiktok branch reads UserAccount->authenticationRandomNumber similarly)
Attack scenario
Attacker requests https://<creator-domain>/creator/otpshow/1001/instagram, /1002/instagram, ... harvesting each user's authenticationRandomNumber, then uses the PIN to mark the social account as verified and impersonate the creator.
Impact

Mass disclosure of social-media ownership-verification PINs for arbitrary users, enabling an attacker to falsely pass Instagram/TikTok ownership verification and chain into account/identity spoofing. Combined with the OTP-login weaknesses it is another impersonation oracle.

Remediation
Move creator/otpshow and creator/linkshow inside the authenticateToken:creator group and add an ownership check ($user_id === auth('creator')->id()). Better: never expose verification PINs through any view/endpoint; deliver them only out-of-band. Switch ids to UUID/Hashids and enforce object-level authorization on every parameterized lookup.
Verifier note

Confirmed: route at creator.php:35 precedes auth group at :91; otpShow returns authenticationRandomNumber with no auth/ownership check.

AP-07

config/thirdparty.php committed exposing live Slack webhooks, Monday.com me:write tokens, and Google OAuth secrets

P0 A02:2021 - Cryptographic Failures CWE-798: Use of Hard-coded Credentials CVSS 9.1 EPSS n/a (credential leak, not a CVE) confirmed
Description

config/thirdparty.php is committed despite being in .gitignore (the rule was added after it was already tracked). It embeds, in cleartext, production+dev Slack incoming-webhook URLs, two Monday.com API tokens (the production one decodes to scope me:write with no expiry), and two complete Google OAuth client_secret+refresh-token pairs. The team's own comment shows they believed the file was protected, so the credentials are unlikely to have been rotated.

Evidence
.gitignore:8-9 has comment '#thirdparty.php contains webhooks for slack' and entry '/config/thirdparty.php', yet git ls-files config/thirdparty.php shows it IS tracked (ignore rule added after the file was committed) config/thirdparty.php:10-17 — 7 production Slack incoming-webhook URLs (signup, approve_decline/offers, vendor, merchant, user_reset, user_reviews) config/thirdparty.php:23 — production Monday.com token eyJhbGciOiJIUzI1NiJ9... (JWT with per:'me:write', no exp); a second dev Monday token at line ~70 config/thirdparty.php:37-38 — Google client_secret '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' and refreshToken '1//09UCiXR2j68ui...'; a second Google client_secret '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' + refresh token in the dev branch
Attack scenario
Attacker clones the repo, POSTs a crafted JSON message to the committed approve_decline Slack webhook to spoof a 'creator approved' notification, and calls the Monday GraphQL API with the me:write token to dump and alter the merchant/applicant boards — no login required.
Impact

A repo-access attacker can post arbitrary messages into A-List's internal Slack channels (phishing operators, spoofing signup/approval/merchant alerts); use the Monday me:write token to read and modify company boards (applicant/merchant CRM data); and use the Google client_secret+refresh token to mint Google API tokens and read the linked account's contacts. Slack webhook URLs are bearer credentials — possession is sufficient.

Remediation
Treat all values as compromised. Regenerate/delete every Slack webhook; revoke the Monday tokens; revoke the Google OAuth client secret and refresh tokens and rotate the client. Move all to env, git rm --cached config/thirdparty.php, scrub history, and verify the gitignore now excludes the untracked file.
Verifier note

Confirmed: file is git-tracked despite .gitignore; production Slack/Monday/Google secrets read verbatim from config/thirdparty.php.

AP-08

Google OAuth client_secret, developer key + long-lived refresh token committed (.config.json, googlePeople.json, token.txt)

P0 A02:2021 - Cryptographic Failures CWE-798: Use of Hard-coded Credentials CVSS 8.6 EPSS n/a (credential leak, not a CVE) confirmed
Description

Multiple committed JSON files expose Google OAuth credentials: .config.json holds a client_secret, an AIza developer key, and a long-lived refresh token; storage/googlePeople.json holds the alist-007 OAuth web client_secret and is loaded by Admin/GoogleAuthController. token.txt is a committed plaintext 20-char token. Refresh tokens do not expire unless revoked, so the committed token can be exchanged for fresh access tokens indefinitely. (The GOCSPX-- secret here is identical to the one in config/thirdparty.php.)

Evidence
.config.json:3 clientSecret 'yXaAHERyiixOjny0_WsaLPLZ'; :5 developerKey 'AIzaSyCSdykEm_jE1KRXksbQfh86NBCRKiyiY7U'; :6 refreshToken '1//06_8SJiX_dBAqCgYIARAAGAYSNwF-...' (committed since initial commit) storage/googlePeople.json — web client for project alist-007 with client_secret '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', redirect_uris admin.alist.ae/auth/google/callback app/Http/Controllers/Admin/GoogleAuthController.php:14 $client->setAuthConfig(storage_path('googlePeople.json')) — committed secret loaded at runtime token.txt — single high-entropy line 'fALezGLFHvHGNWBZtTTV' committed at repo root (no code references found) all four files git-tracked per git ls-files
Attack scenario
Attacker takes clientSecret + refreshToken from .config.json, POSTs grant_type=refresh_token to https://oauth2.googleapis.com/token to obtain an access token, then calls the People API to exfiltrate the company's Google contacts; separately reuses the AIza key to run up Google billing.
Impact

A repo-access attacker can mint Google API access tokens via client_secret+refresh_token and read/modify the linked account's contacts (People API), abuse the AIza key (billing/quota theft, any unrestricted Google APIs), and conduct OAuth phishing against the admin.alist.ae callback. token.txt is a ready-to-use credential of unknown scope.

Remediation
Revoke and rotate the OAuth client secret, the AIza developer key, and all refresh tokens; revoke the token.txt value once its service is identified. Remove .config.json, storage/googlePeople.json, and token.txt from tree and history; load via env/secret store. IP/referrer-restrict the AIza key. Note the GOCSPX-- secret is shared with thirdparty.php — rotate once and update all consumers.
Verifier note

Confirmed by reading .config.json, storage/googlePeople.json, GoogleAuthController.php:14, token.txt. Merges raw findings #3, #10, and #34 (auth_jwt duplicate).

AP-09

Multiple authenticated SQL injection in UsersPreviewsController::filterPreviews (DataTables endpoint)

P1 A03:2021 - Injection CWE-89: SQL Injection CVSS 8.1 confirmed
Description

filterPreviews() builds raw SQL passed to DB::select() by concatenating at least four request-controlled inputs with no escaping or binding: the DataTables search value (7 LIKE clauses), the paging params start/length (into LIMIT $start,$limit), each $venue value (into a subquery WHERE b.restaurant_name = ...), and $instagram_var (into an IN list). No bindings are used.

Evidence
app/Http/Controllers/Admin/UsersPreviewsController.php:208 $search = $request->input('search.value'); UsersPreviewsController.php:275 $whereCondition .= " AND ( name LIKE '%".$search."%' OR ... OR venue_title LIKE '%".$search."%')"; UsersPreviewsController.php:279 DB::select("SELECT ... WHERE is_approved =1 ".$whereCondition." ORDER BY name ASC LIMIT $start,$limit") — $start/$limit from request->input('start'/'length') interpolated raw into LIMIT UsersPreviewsController.php:237 ' and name in (select ... WHERE b.restaurant_name = '.$venue.')' ($venue from explode of $request->venues); :269 " AND c.instagram_followers in (".$instagram_var.")" routes/admin.php:228 Route::get('filterPreviews','UsersPreviewsController@filterPreviews') inside the ['auth','permission'] group
Attack scenario
An operator account sends instagram_followers=1) UNION SELECT id,access_token,3,... FROM oauth_access_tokens-- to /filterPreviews, exfiltrating Passport tokens that then allow impersonating any creator on the mobile API.
Impact

An authenticated low-tier admin/operator (or hijacked session) can read or modify the entire MySQL database via UNION/boolean/time-based injection: creator PII, Passport OAuth tokens, Stripe references, admin password hashes. With MySQL FILE privilege this can escalate to file read/write (INTO OUTFILE webshell) and RCE on the app host.

Remediation
Replace the string-built DB::select with the Query Builder used elsewhere in the file. Cast $start/$limit with (int); pass search/venue/follower values as bound parameters (? placeholders or whereRaw('... LIKE ?', ['%'.$search.'%'])); validate venues/instagram_followers as integer arrays and use whereIn(). Never interpolate request data into SQL.
Verifier note

Confirmed at UsersPreviewsController.php:208/237/269/275/279; all four inputs concatenated into DB::select with no bindings.

AP-10

SQL injection in AuditController::listCategoryVenues ($request->cid into WHERE)

P1 A03:2021 - Injection CWE-89: SQL Injection CVSS 8.1 confirmed
Description

listCategoryVenues() concatenates the unvalidated, uncast $request->cid directly into a DB::select in numeric (unquoted) context, allowing fully-unquoted UNION/boolean/time-based payloads. The JSON response returns rows directly, enabling trivial UNION extraction.

Evidence
app/Http/Controllers/Admin/AuditController.php:106 $category = $request->cid; AuditController.php:110 DB::select("SELECT id,venue_title FROM venues WHERE category_id =" . $category . " ORDER BY venue_title ASC") routes/admin.php:273 Route::get('listCategoryVenues','AuditController@listCategoryVenues') (auth+permission group)
Attack scenario
Operator requests /listCategoryVenues?cid=0 UNION SELECT user_id,access_token FROM oauth_access_tokens-- and reads tokens from the JSON response.
Impact

Authenticated admin/operator can extract or alter any table via UNION/boolean/time-based injection, including creator PII and OAuth tokens; potential FILE-privilege escalation to RCE.

Remediation
Cast to integer ($category = (int)$request->cid) and use a bound parameter: DB::select('SELECT id,venue_title FROM venues WHERE category_id = ? ...', [$category]). Prefer Venues::where('category_id',$category)->...
Verifier note

Confirmed at AuditController.php:106/110.

AP-11

SQL injection in FoodOffersController::venueDropListAuto ($request->keyword into LIKE)

P1 A03:2021 - Injection CWE-89: SQL Injection CVSS 8.1 confirmed
Description

venueDropListAuto() inlines $request->keyword into a LIKE '%...%' literal with no escaping; a single quote breaks out and UNION/blind payloads read arbitrary tables. Results returned as JSON.

Evidence
app/Http/Controllers/Admin/FoodOffersController.php:2243 $seacrh = $request->keyword; FoodOffersController.php:2245 DB::select("SELECT id,venue_title,category_id,skip_stories FROM venues WHERE venue_title LIKE '%" . $seacrh . "%' ORDER BY id ASC") routes/admin.php:164 Route::get('venueDropListAuto','FoodOffersController@venueDropListAuto') (auth+permission group)
Attack scenario
Operator requests /venueDropListAuto?keyword=%' UNION SELECT id,name,email,instagram_url FROM signups-- to dump creator PII.
Impact

Full read/modify of the MySQL database for any authenticated back-office user; potential FILE-privilege escalation to RCE.

Remediation
Use a bound parameter: DB::select("... WHERE venue_title LIKE ? ...", ['%'.$seacrh.'%']); or the Query Builder where('venue_title','LIKE',"%$seacrh%").
Verifier note

Confirmed at FoodOffersController.php:2243/2245.

AP-12

SQL injection in AdminController::userActionHistory ($request->id into WHERE user_id)

P1 A03:2021 - Injection CWE-89: SQL Injection CVSS 8.1 confirmed
Description

userActionHistory() concatenates uncast $request->id directly after user_id= in numeric context, allowing UNION/boolean/time-based injection. The same uncast-id anti-pattern recurs throughout AdminController.

Evidence
app/Http/Controllers/Admin/AdminController.php:3203 $id = $request->id; AdminController.php:3204 DB::select("SELECT * FROM users_actions where user_id=" . $id) routes/admin.php:383 Route::get('userActionHistory','AdminController@userActionHistory') (auth+permission group)
Attack scenario
Operator requests /userActionHistory?id=1 OR SLEEP(5) to confirm injection, then UNION-extracts data.
Impact

Authenticated back-office user can read/alter the entire database via injection; potential RCE via FILE privilege.

Remediation
Cast ($id = (int)$request->id) and bind: DB::select('SELECT * FROM users_actions WHERE user_id = ?', [$id]). Globally migrate the repeated DB::select("... = ".$id) pattern to parameterized queries or Eloquent.
Verifier note

Confirmed at AdminController.php:3203/3204.

AP-13

Blind SQL injection in DashboardController::getReviewWidget (startDate/endDate into DB::select)

P1 A03:2021 - Injection CWE-89: SQL Injection CVSS 7.6 confirmed
Description

getReviewWidget() interpolates $request->startDate/endDate inside single-quoted SQL literals and appends to a DB::select with no binding. An attacker escapes the quote to inject SQL; only an aggregate count is reflected, so extraction is boolean/time-based blind.

Evidence
app/Http/Controllers/Admin/DashboardController.php:1078 $startDate = $request->startDate; :1082 $endDate = $request->endDate; DashboardController.php:1087 $datesQuerystr = " and `O`.`offer_date` between '$startDate' and '$endDate'"; DashboardController.php:1092 $reviewAuditquery = DB::select("select count(O.id) ... and `U`.`id` is null $datesQuerystr") routes/admin.php:120 Route::get('reviewWidget','DashboardController@getReviewWidget') (auth+permission group)
Attack scenario
Operator sets endDate=2025-01-01' AND (SELECT SUBSTRING(password,1,1) FROM admins LIMIT 1)='a' AND SLEEP(5)-- and reads the timing to extract admin hashes character by character.
Impact

Authenticated back-office user can perform blind SQLi to extract any DB contents one condition at a time and run DoS-inducing time-delay queries against production.

Remediation
Bind the date range with ? placeholders passed to DB::select, or use the Query Builder whereBetween('O.offer_date',[$startDate,$endDate]). Validate startDate/endDate with the 'date' rule.
Verifier note

Confirmed at DashboardController.php:1078/1082/1087/1092.

AP-14

Second-order SQL injection via venue avail_days interpolated into offer-eligibility whereRaw on the mobile API hot path

P2 A03:2021 - Injection CWE-89: SQL Injection CVSS 6.5 confirmed
Description

avail_days comes straight from $request->avail_days in VenuesController store()/update() with no validation, and the Venues model has no fillable/cast, so a non-numeric string is stored verbatim. That stored value is later concatenated unescaped into many whereRaw() INTERVAL clauses executed on the public mobile-app offer-eligibility path. A venue-editing operator who sets avail_days to a payload (e.g. 1 DAY) OR SLEEP(5) -- ) injects SQL that fires whenever a creator browses offers for that venue. (Note: the sibling $offerDate interpolations are server-computed via date() and not injectable.)

Evidence
app/Http/Controllers/Admin/VenuesController.php:729 $venue->avail_days = $request->avail_days; (store()/update() validate only 'venue_title' — VenuesController.php:653-655) app/Venues.php:14 — $fillable is commented out and no $casts, so the raw string persists app/Repositories/OfferRepository.php:6218 ->whereRaw('DATE_ADD(food_offers_users.updated_at, INTERVAL ' . $offer->avail_days . ' DAY) > ?', [Carbon::now()]) (same pattern at :6580 and in OfferController/UserAuthController offer flows)
Attack scenario
Operator edits a venue and sets avail_days to a SQL payload; later, any creator opening that venue's offers in the app triggers the injected query (e.g. SLEEP-based blind extraction), executing under the public API with no further admin action.
Impact

An operator with venue-edit rights can plant a stored SQL payload that executes in the high-traffic creator-facing API offer query, enabling blind data extraction and time-based DoS against production. Rated Low CIA because the result is not reflected (blind) and storage requires an authenticated venue editor.

Remediation
Validate avail_days (and max_campaign_visible_days) as integers at intake ('avail_days' => 'nullable|integer|min:0') and add an integer cast on the Venues model. At the sink cast before use: 'INTERVAL ' . (int)$offer->avail_days . ' DAY', or use a bound placeholder. Apply to every avail_days whereRaw occurrence.
Verifier note

Confirmed: VenuesController.php:729 stores raw input; app/Venues.php:14 fillable commented; OfferRepository.php:6218 interpolates avail_days into whereRaw.

AP-15

Unauthenticated API write: arbitrary review-column overwrite (IDOR + dynamic property) and unauthenticated image-upload sinks

P1 API1:2023 Broken Object Level Authorization CWE-862: Missing Authorization (with CWE-915) CVSS 8.1 confirmed
Description

The legacy /api/* route file is mounted with only throttling and route-model-binding, no authentication. review/update combines missing authorization (IDOR via review_id) with a dynamic-property write ($review->$field_name = $upload), letting an anonymous attacker set an arbitrary column on any user's review row to a server-controlled uploaded-file path. offers/save-banner and category/save-image are unauthenticated image-upload sinks usable for storage abuse and as part of upload/decoder attacks.

Evidence
routes/api.php:27 has no auth middleware (the auth:api line is commented out); RouteServiceProvider::mapApiRoutes applies only the 'api' group (throttle:500,1 + bindings) routes/api.php:31 POST offers/save-banner -> OfferController@offersBannerSaveAPI (unauthenticated image upload) routes/api.php:38 POST review/update -> UserReviewController@reviewUpdateAPI app/Http/Controllers/UserReviewController.php:528-554 $review = UsersReviews::find($request->review_id) (no ownership check) then $review->$field_name = $upload; $review->save(); — BOTH review_id and field_name attacker-controlled routes/api.php:43 POST category/save-image -> SettingsController@categoryImageUploaderAPI (unauthenticated)
Attack scenario
Attacker POSTs to /api/review/update with review_id=<enumerated>, field_name=is_approved, image=<file> (or sets any guessable column) to tamper with arbitrary creators' review records without authentication.
Impact

Anonymous tampering with any creator's review record (e.g. clobbering an approval-status or screenshot column by guessing the name), plus unauthenticated upload of arbitrary images into public asset directories. Sequential review_id makes the IDOR trivially enumerable.

Remediation
Apply auth:api to every state-changing route in routes/api.php. In reviewUpdateAPI, scope the lookup to the authenticated user and replace the dynamic $review->$field_name write with an allowlist mapping request keys to a fixed set of screenshot columns.
Verifier note

Confirmed: routes/api.php has no auth, reviewUpdateAPI uses ::find($request->review_id) and dynamic $review->$field_name with no ownership check.

AP-16

Creator portal object-ID routes (review viewing, offer manipulation) declared before the authenticateToken group

P1 A01:2021 - Broken Access Control CWE-862: Missing Authorization CVSS 7.5 confirmed
Description

A large block of creator-portal routes taking object identifiers (review and offer IDs) is registered before the authenticateToken:creator group. The file-level creator group provides only session/CSRF, not authentication, so these routes are reachable anonymously via forced browsing, with the object ID controlling which user's review/offer is read or acted upon.

Evidence
routes/creator.php:91 Route::group(['middleware' => ['authenticateToken:creator']]) is where creator auth begins routes/creator.php:59-90 (before that group) define GET routes with object IDs and no auth: review/{id}, review/{id}/archive, userReview/{id}, userReview/{id}/{id1}, rejectedreview/{id}/{id1}, viewDedicatedOfferStatus/{id}, offerViewInviteCancel/{id}, venueRedeem/{id}/{id1?}/{id2?} (Route::any), deleteOfferUsers, proceedOffer, addnewInvitation, offerInvitationDetails/{id} RouteServiceProvider::mapCreatorRoutes applies only the 'creator' group (session/CSRF, no auth)
Attack scenario
Attacker iterates GET /review/1, /review/2, ... to read arbitrary creators' review submissions, and calls /deleteOfferUsers / /offerViewInviteCancel/{id} to manipulate offers without authenticating.
Impact

Unauthenticated forced browsing of any creator's review submissions and rejected-review details (IDOR), plus reachability of offer-manipulation actions (delete offer users, cancel invitations) without a creator session.

Remediation
Move routes/creator.php:59-90 inside the authenticateToken:creator group, then add per-object ownership checks (scope every review/offer lookup by the authenticated creator id). Apply authentication at the file/group level in RouteServiceProvider.
Verifier note

Confirmed: creator.php:59-90 routes precede the auth group at :91; creator middleware group has no auth.

AP-17

IDOR: any creator can delete another user's notification (NotificationRepository::delete)

P1 API1:2023 Broken Object Level Authorization CWE-639: Authorization Bypass Through User-Controlled Key CVSS 6.5 confirmed
Description

The mobile-API notification delete endpoint looks up the notification by request-supplied id via Notification::find() and deletes it without verifying ownership. Any authenticated creator can delete any other creator's notification by iterating sequential integer IDs.

Evidence
routes/api_v1.php:327 POST notification/delete -> NotificationController@delete (inside auth:api group) app/Http/Controllers/Api/v1/NotificationController.php validates only that id is present, then calls $this->repo->delete($request->all()) app/Repositories/NotificationRepository.php:49-60 $data = Notification::find($requestData['id']); if ($data) { $data->delete(); } — NO where('user_id', auth()->id()) scope Contrast NotificationRepository.php:37-40 all() which DOES scope ->where('user_id', auth()->user()->id), confirming the omission is a bug
Attack scenario
Authenticated creator scripts POST /v1/notification/delete with id=1..N, deleting every other creator's notifications.
Impact

Horizontal privilege escalation / integrity loss: an authenticated creator can mass-delete other creators' invitation, offer, and review notifications via ID enumeration, suppressing time-sensitive offer invitations and reminders.

Remediation
Scope the delete to the owner: Notification::where('id',$requestData['id'])->where('user_id',auth()->id())->first()->delete(); return not-found when zero rows match.
Verifier note

Confirmed: NotificationRepository.php:51 ::find with no user_id scope vs all() at :38 which does scope.

AP-18

Global CORS middleware sets Access-Control-Allow-Origin: * on every response (incl. authenticated /v1 API and admin)

P1 A05:2021 - Security Misconfiguration CWE-942: Permissive Cross-domain Policy with Untrusted Domains CVSS 7.5 confirmed
Description

A custom Cors middleware in the global stack unconditionally emits Access-Control-Allow-Origin: * with broad Allow-Methods/Allow-Headers (including Authorization) on every response, including the Passport-protected /v1 API and the cookie-authenticated admin panel. Laravel's HandleCors is not registered, so config/cors.php is dead code. With the literal wildcard, browsers won't attach cookies, but any web origin can read responses of endpoints that authorize via a request-attached token or that return sensitive data without per-request credentials (data-leaking endpoints, account-existence oracles, OTP-echo endpoint).

Evidence
app/Http/Middleware/Cors.php:20 $response->headers->set('Access-Control-Allow-Origin','*'); Cors.php:21-22 Allow-Methods 'POST, GET, OPTIONS, PUT, DELETE' and Allow-Headers incl. Authorization, X-API-KEY app/Http/Kernel.php:24 \App\Http\Middleware\Cors::class is in the GLOBAL $middleware stack -> runs on every request routes/api_v1.php:29 re-applies the same custom Cors handler to the mobile API; Laravel's HandleCors is not wired in (config/cors.php is inert)
Attack scenario
A victim with an A-List bearer token visits attacker.com, whose JS issues fetch('https://api/v1/...', {headers:{Authorization}}) cross-origin and reads the JSON response (e.g. profile PII) because of the wildcard ACAO.
Impact

The Same-Origin Policy is effectively disabled for read access to all API responses. Any malicious site can issue cross-origin requests and read JSON for any endpoint not strictly gated by an HttpOnly session cookie, exposing creator/merchant PII, offer eligibility data, and account-existence oracles, and broadening the cross-origin attack surface.

Remediation
Delete the custom Cors middleware. Register Laravel's HandleCors and rely on config/cors.php with an explicit first-party origin allowlist, supports_credentials only where required, never the '*' literal, scoped to api/v1 paths. Do not emit CORS headers on the admin/web HTML app.
Verifier note

Confirmed: Cors.php:20 wildcard; Kernel.php:24 global; HandleCors not registered.

AP-19

Server-side fetch of user-supplied URL via file_get_contents (SSRF / LFI) on creator and signup routes

P1 A10:2021 - Server-Side Request Forgery (SSRF) CWE-918: Server-Side Request Forgery CVSS 8.6 confirmed
Description

Multiple controllers pass an unvalidated request field (image1..image6) straight into file_get_contents()/Intervention ImageResize::make(), performing a server-side fetch of an attacker-chosen URL. There is no scheme allowlist, no host allowlist, and no block of internal/link-local ranges. ImageResize rejects non-image bytes, but the outbound request/local read has already executed, giving blind SSRF and (for file://) server-side file disclosure into the resize pipeline. Reachable by low-privilege creators (review/insight submission) and signup flows.

Evidence
app/Http/Controllers/UserReviewController.php:163-169 if ($request->image1 != null){ $file1 = $request->image1; ... $base64Image1 = file_get_contents($file1); ImageResize::make($base64Image1)->save($path1); } (repeats image2..image5) app/Http/Controllers/SignupController.php:709 $file1 = $request->image1; ... fed to file_get_contents/ImageResize in instagramInsightStore (image1..image6) routes/creator.php reviewSave -> UserReviewController@store; no scheme allowlist, no private-IP block, no timeout/size cap
Attack scenario
A creator submits reviewSave with image1=http://169.254.169.254/latest/meta-data/iam/security-credentials/<role>; the server fetches the IMDS endpoint, and even with ImageResize rejecting the bytes the request succeeds, enabling SSRF and (with file://) local-file disclosure.
Impact

Pivot from the public/creator surface into the internal AWS network: query EC2 instance metadata (169.254.169.254) for IAM credentials, reach the internal ElastiCache Redis endpoint, port-scan internal services, and read local files via file:// (e.g. the committed Firebase key). Blind SSRF alone enables internal mapping and IMDS abuse.

Remediation
Never pass raw request strings to file_get_contents/ImageResize::make. Require multipart file uploads ($request->file()) with MIME/extension/size validation. If remote fetch is required, enforce an https-only scheme allowlist, resolve and reject RFC1918/loopback/link-local/IMDS, disable redirects, set strict timeouts and max size, and use a dedicated egress proxy. Migrate to IMDSv2.
Verifier note

Confirmed: UserReviewController.php:168 and SignupController.php:709 pass $request->imageN to file_get_contents with no validation.

AP-20

End-of-life PHP 7.3 runtime pinned in production (.htaccess ea-php73), mismatched with composer ^8.2

P1 A06:2021 - Vulnerable and Outdated Components CWE-1104: Use of Unmaintained Third Party Components CVSS 8.1 EPSS varies by interpreter CVE; PHP 7.3 EOL since 2021-12-06 (no patches) confirmed
Description

The production web-server handler is pinned to PHP 7.3 (cPanel ea-php73), an EOL interpreter with no security patches since December 2021 and numerous unpatched memory-safety/parser CVEs (GD, EXIF, phar, libxml). The codebase simultaneously requires PHP ^8.2, so the deployment is in an inconsistent state — either silently running on an unsupported runtime or built against a different PHP than serves traffic.

Evidence
.htaccess:110 AddHandler application/x-httpd-ea-php73 .php .php7 .phtml .htaccess:89/101 session.save_path "/var/cpanel/php/sessions/ea-php73" error_log contains hundreds of /ea-php73/ path references corroborating the deployed runtime composer.json requires php ^8.2 while Laravel 11.48 (composer.lock) requires php >=8.2 — inconsistent with the live PHP 7.3 handler
Attack scenario
An attacker submits a crafted image to one of the unauthenticated image-decode routes; on the EOL GD/Imagick stack of PHP 7.3 a known decoder CVE triggers memory corruption or DoS that an 8.2 runtime would have patched.
Impact

An unpatched PHP 7.3 interpreter exposes the host to interpreter and bundled-extension CVEs (image/EXIF/phar/libxml memory corruption) that no application control can mitigate, especially given attacker-controlled image processing. No upstream fix path exists; only a runtime upgrade remediates.

Remediation
Upgrade the cPanel MultiPHP handler to PHP 8.2/8.3 matching composer.json and remove the ea-php73/ea-php72 handler blocks. Run composer install --no-dev on the same PHP that serves traffic. Add a php -v deploy gate enforcing >=8.2.
Verifier note

Confirmed: .htaccess:110 ea-php73 handler; composer.lock laravel/framework v11.48.0 requires php >=8.2.

AP-21

Hardcoded backdoor OTP: subin@alist.ae always accepts 1122

P1 A07:2021 Identification and Authentication Failures CWE-798: Use of Hard-coded Credentials CVSS 7.2 confirmed
Description

The OTP-login flow contains a hardcoded static OTP: for subin@alist.ae the OTP is always 1122. The comment states it was added for an app-store review account. This is a permanent backdoor: anyone who knows (or guesses) this internal staff email can log in with the fixed code 1122 via the unauthenticated mobile login endpoint.

Evidence
app/Repositories/SignupRepository.php:2211-2216 // Modification in sending otp for app approval ... if($signup->email == "subin@alist.ae") { $otp = 1122; } else { $otp = $this->generateNDigitRandomNumber(4); } Reached via routes/api_v1.php:65 login/with-otp (unauthenticated) -> SignupController::loginWithOTP -> SignupRepository::loginWithOTP
Attack scenario
Attacker POSTs {email:'subin@alist.ae'} to /v1/login/with-otp then {user_id, otp:1122} to /v1/login/verify-otp and logs in as the staff account.
Impact

Permanent, publicly-known credential bypass for a specific internal/staff account. If that account carries elevated privileges or is staff-used, it is a direct takeover vector and normalizes backdoor patterns.

Remediation
Remove the subin@alist.ae / 1122 branch entirely. For app-store review use a dedicated low-privilege test account whose OTP arrives via the normal channel, or a time-boxed feature flag disabled in production. Grep for other email==literal / otp==literal shortcuts.
Verifier note

Confirmed at SignupRepository.php:2212-2214.

AP-22

PIN/OTP secrets stored in plaintext and compared with loose equality (no hashing)

P1 A02:2021 Cryptographic Failures CWE-256/257: Plaintext Storage of Recoverable Credentials CVSS 6.5 confirmed
Description

Login PINs and login/verification OTPs are stored as plaintext columns and validated via SQL equality or PHP loose == comparison, never hashed. The Signup model hides only api_token (not pin/otp), so these secrets can leak through any code path that serializes the model. Plaintext storage means a single DB read exposes every user's login factor; loose == also invites type-juggling.

Evidence
app/Repositories/SignupRepository.php:1120 ->where('pin', $requestData['pin']) — login PIN matched as plaintext column SignupRepository.php:1270/1299 PIN persisted in cleartext; :2218 $signup->otp = $otp persisted cleartext; :913 if ($creator->pin == $otp) loose == SignupRepository.php:2731 if ($requestData['otp'] == $brand->otp) cleartext == comparison app/Signup.php:35 $hidden = ['api_token'] — pin and otp are NOT hidden, so they can surface via model serialization
Attack scenario
An attacker who achieves any DB read (e.g. via one of the confirmed admin SQLi) selects pin/otp columns from signups and logs in as any user without brute force.
Impact

Any read access to the database (SQLi, backup leak, insider) yields every user's current login PIN/OTP, enabling mass account takeover. Plaintext handling and loose comparison further weaken the factor.

Remediation
Hash PINs with bcrypt/argon2 and verify with Hash::check; treat OTPs as one-time hashed tokens with short TTL and constant-time comparison (hash_equals). Use strict comparison. Add pin/otp/authenticationRandomNumber to $hidden. Migrate existing plaintext values and force re-issue.
Verifier note

Confirmed: SignupRepository.php:1120/2218/2731 plaintext+loose==; Signup.php:35 hidden lacks pin/otp.

AP-23

Long-lived Passport access tokens with no logout/password-change revocation

P1 API2:2023 Broken Authentication CWE-613: Insufficient Session Expiration CVSS 6.5 confirmed
Description

No Passport token lifetime is configured, so access tokens use the long default expiry (~1 year). The mobile v1 API exposes no logout/revocation route, and tokens are only purged on the next successful login by the same user. The creator alistToken cookie is set to the 30-day session lifetime. There is no password-change-triggered revocation (these users authenticate by PIN/OTP).

Evidence
app/Providers/AuthServiceProvider.php:30 boot() only calls Passport::loadKeysFrom() — NO tokensExpireIn/refreshTokensExpireIn/personalAccessTokensExpireIn override (grep returns none) => Passport long default (~1 year) Tokens minted as personal access tokens: SignupRepository.php ~2288 and CreatorController.php:240 createToken('Alist Personal Access Client')->accessToken config/session.php:34 SESSION_LIFETIME default 43200 (30 days); CreatorController.php:241 sets alistToken cookie TTL to session.lifetime Token cleanup only on next successful login ($signup->tokens()->delete()); routes/api_v1.php has no /logout route
Attack scenario
A creator's bearer token leaks via a referer/log; months later the attacker replays it against /v1 and still has full access because the token never expired and there is no logout/revocation.
Impact

A stolen/leaked bearer token remains valid for months with no user-facing revocation, greatly enlarging the window for token-replay account takeover after any leak (XSS, log/Referer leak, device theft).

Remediation
Set Passport::tokensExpireIn (e.g. 1 hour) with refresh-token rotation. Add an authenticated /v1/logout that revokes the token. Revoke all tokens on PIN/OTP reset. Reduce SESSION_LIFETIME and the alistToken cookie lifetime to a sane value.
Verifier note

Confirmed: AuthServiceProvider boot has no expiry override; no /logout in api_v1.php; SESSION_LIFETIME default 43200.

AP-24

Brute-forceable 4-digit venue redemption PIN (weak mt_rand, no per-offer attempt lockout)

P1 A07:2021 Identification and Authentication Failures / A04:2021 Insecure Design CWE-330: Use of Insufficiently Random Values (with CWE-307) CVSS 7.1 confirmed
Description

Each venue's redemption is gated by a single shared 4-digit numeric PIN (venu_code) generated with mt_rand and never rotated per redemption. /v1/offers/verify/venue-code accepts a client-supplied venue_code and on string match flips the voucher's used_at to now. There is no per-offer/per-venue failed-attempt counter, lockout, or step-up — only the platform-wide 500/min throttle. The ~9000 keyspace is exhaustible.

Evidence
app/Http/Controllers/Admin/VenuesController.php:1662-1666 generateNDigitRandomNumber(4) => mt_rand(1000,9999), ~9000 values database migration alter_venu_table — venu_code is a 4-digit integer app/Repositories/OfferRepository.php:7542 if ($offerCode->foodOffers->venue->venu_code == $venue_code) — verifyVenueCode compares client-supplied 4-digit PIN; on match marks voucher used. No per-venue/per-offer attempt counter routes/api_v1.php:193 offers/verify/venue-code protected only by auth:api; only global throttle:500,1
Attack scenario
A creator holding a valid offer code scripts /v1/offers/verify/venue-code with venue_code iterating 1000-9999 against their own code_id; on the matching PIN the voucher is marked redeemed without the venue's involvement.
Impact

An authenticated creator can brute-force any venue's static PIN and self-confirm redemptions without the venue scanning/approving, defeating merchant verification. Because the PIN is per-venue and static, one cracked PIN compromises every redemption at that venue indefinitely. Combined with the redemption race this enables full self-service fraud.

Remediation
Replace the static PIN with a high-entropy per-redemption single-use token (random_bytes) bound to the specific food_offers_users row, short-lived. Add a strict per-offer/per-venue attempt limiter with backoff/lockout. Use a CSPRNG everywhere a secret is minted.
Verifier note

Confirmed: OfferRepository.php:7542 compares venu_code; generateNDigitRandomNumber 4-digit; no lockout in verifyVenueCode.

AP-25

Race condition (TOCTOU) on offer/voucher redemption with transactions explicitly disabled (double-spend)

P1 A04:2021 Insecure Design CWE-362: Race Condition (with CWE-367 TOCTOU) CVSS 8.1 confirmed
Description

The creator offer-redemption flow performs eligibility/availability checks with a SELECT then mutates shared food_offers_users rows with a separate UPDATE/save(), with no DB transaction (DB::beginTransaction is commented out) and no row-level locking. The helper named fetchAndLockOfferCode does not lock. Vouchers are a finite pre-seeded pool (user_id=NULL, block=NULL); the 'block' column is set non-atomically. Concurrent requests bypass the 'already applied' guard and allocate the same voucher / oversubscribe a date's capacity.

Evidence
app/Repositories/OfferRepository.php:5057 // DB::beginTransaction(); — transaction commented out in AcceptOffer; no commit/rollback in the redemption path OfferRepository.php:4185-4216 fetchAndLockOfferCode() does FoodOffersUsers::where([...['block',null]])->first() then $voucherCode->block = 1; ->save() with NO lockForUpdate/transaction — two concurrent requests both read block==null and both claim the same code OfferRepository.php:7539-7548 verifyVenueCode: if ($offerCode->used_at == null) { ... ->update(['used_at'=>...]) } — TOCTOU letting the same voucher be marked used by concurrent calls app/Http/Kernel.php:85-86 api throttle:500,1 is wide enough to fire many concurrent redeems; routes/api_v1.php:144 offers/accept carries only auth:api
Attack scenario
An attacker fires 30 concurrent POSTs to /v1/offers/accept for the same single-use offer; the un-transacted check-then-act admits several, allocating the same voucher multiple times and exceeding the venue-funded quota.
Impact

An influencer can double-spend a single-use offer and overrun a campaign's seeded voucher inventory, redeeming more merchant-funded perks than allocated — direct financial loss to merchants. verifyVenueCode TOCTOU also lets one voucher be flipped used by parallel requests, corrupting redemption accounting.

Remediation
Wrap each redemption in DB::transaction() and select the candidate voucher with ->lockForUpdate() before mutating; re-check eligibility inside the lock. Replace SELECT-then-save with a single conditional UPDATE (... WHERE id=? AND user_id IS NULL AND block IS NULL) treating affected-rows=0 as already taken. Add a unique constraint on (offer_id,user_id) and an idempotency key / tight per-action throttle on offers/accept.
Verifier note

Confirmed: OfferRepository.php:5057 transaction commented; fetchAndLockOfferCode 4185-4216 has no lock; verifyVenueCode used_at TOCTOU.

AP-26

Hard-coded production database credentials as env() fallback in config/database.php

P1 A05:2021 - Security Misconfiguration CWE-798: Use of Hard-coded Credentials CVSS 7.5 EPSS n/a (credential leak, not a CVE) confirmed
Description

The MySQL config hard-codes username parhaman_admin and password !0325Parham! as the env() default used when DB_USERNAME/DB_PASSWORD are unset. This leaks production-style DB credentials into source and history and is a silent footgun: any environment missing the env vars authenticates with these baked-in values. The 'parhaman' naming matches the hosting path leaked in error_log, indicating they are/were live.

Evidence
config/database.php:52 'username' => env('DB_USERNAME', 'parhaman_admin'), config/database.php:53 'password' => env('DB_PASSWORD', '!0325Parham!'), username 'parhaman' matches the leaked cPanel path /home/parhaman/ in the committed error_log
Attack scenario
An attacker with repo access reads the DB password and, via a reachable MySQL endpoint (SSRF, misconfigured security group, or co-tenant on the cPanel box), authenticates as parhaman_admin and dumps the platform database.
Impact

Disclosure of database credentials to anyone with repo access; if the MySQL host is reachable these grant direct read/write to the entire platform DB (creator/merchant PII, offers, sessions). The value persists in history even if rotated.

Remediation
Remove the hard-coded fallback (use env('DB_USERNAME')/env('DB_PASSWORD') with null default). Rotate the MySQL password if !0325Parham! was ever used in production. Scrub history. Make deploys fail closed when env vars are missing.
Verifier note

Confirmed at config/database.php:52-53.

AP-27

Hard-coded Stripe secret API keys in StripeController and SubscriptionController

P1 A05:2021 - Security Misconfiguration CWE-798: Use of Hard-coded Credentials CVSS 7.5 EPSS n/a (credential leak, not a CVE) confirmed
Description

Stripe SECRET keys (sk_test_*) and the webhook signing secret (whsec_) are hard-coded directly in controller source rather than read from config/env. StripeController::getSession instantiates the client with a literal secret key; SubscriptionController has a second commented secret key and a hardcoded whsec_. As written they are test-mode keys, but committing secret keys is the dangerous pattern — a Stripe secret key grants full server-side API access.

Evidence
app/Http/Controllers/StripeController.php:20 new \Stripe\StripeClient('sk_test_51MoLW9SGejZN1wpelEL90vafKNu1CYcE4hlPCctJsxoBLqMiDIKL7WJOHGTPnI5ppV3fE9V4zLUoJslr8AVvyy1x00DNKNRqx4'); app/Http/Controllers/SubscriptionController.php:42 //Stripe::setApiKey("sk_test_51KVfKi..."); (commented but committed) SubscriptionController.php:55 hardcoded webhook signing secret "whsec_9oyth6laBnb2kDZCQdBYVFPW58k9v8iX" (config call commented out)
Attack scenario
An attacker reading the repo copies the sk_test key and calls the Stripe API to enumerate customers/payment objects; the leaked whsec_ also lets an attacker forge valid webhook signatures.
Impact

Possession of a Stripe secret key allows full API access to the corresponding account (charges, refunds, customer/payment data). The hardcoded whsec_ cannot be rotated via config and is exposed to anyone with repo access. The same hardcoding habit applied to a live key would be direct financial/PCI exposure.

Remediation
Move all Stripe keys to env (config('services.stripe.secret') and webhook_secret); never inline secret keys. Roll the exposed sk_test and whsec_ values in the Stripe dashboard. Remove the commented secret. Scrub history and add secret scanning to CI.
Verifier note

Confirmed at StripeController.php:20, SubscriptionController.php:42/55.

AP-28

RSA private key (alist-dev-aswin.pem) recoverable from git history

P1 A02:2021 - Cryptographic Failures CWE-321: Use of Hard-coded Cryptographic Key CVSS 7.5 EPSS n/a (credential leak, not a CVE) confirmed
Description

A full PEM-encoded RSA private key (named like an SSH/deploy key for developer 'aswin') was committed then deleted from the working tree but remains fully recoverable from git history. Deleting a file in a later commit does not remove it from history; the key is still distributed in every clone. The reactive .gitignore entries confirm the team knew the keys leaked.

Evidence
git log --all -- alist-dev-aswin.pem: added in 6f6b81d93 'broadcast config change', removed in 348582761 'config change' git show 6f6b81d93:alist-dev-aswin.pem begins '-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwD3TVQBLhRUPF2lNOjz4gYo4ytNrH263/...' (recovered verbatim) .gitignore lists 'dev-alist-key.pem' and 'alist-dev-aswin.pem' — added only after the key was already committed
Attack scenario
An attacker clones the repo, runs git log --all to find the deleted pem, extracts the key from the historical blob, and uses it as an SSH key against the deploy host if the public key is still trusted.
Impact

If still authorized (SSH deploy/access key, broadcast/cert/signing key), anyone with repo/history access can authenticate to whatever trusts it — potentially server access or impersonation in the broadcast pipeline.

Remediation
Treat as compromised: remove its public key from all authorized_keys/trust stores and rotate the pair. Purge alist-dev-aswin.pem from git history (filter-repo/BFG) and force-push. Confirm no other *.pem exists in history.
Verifier note

Confirmed by git show 6f6b81d93:alist-dev-aswin.pem returning the RSA key.

AP-29

Web-accessible sensitive files in document root: phpinfo() pages, committed error logs, plaintext token, standalone OAuth script

P1 A05:2021 - Security Misconfiguration CWE-538: Insertion of Sensitive Information into Externally-Accessible File (with CWE-200) CVSS 7.5 confirmed
Description

The repository root is the Apache document root (.htaccess rewrites to /public only when the requested file does not already exist, per RewriteCond !-f), so committed literal files at root/public are served directly. phpinfo() pages disclose full server config, env vars, modules, and paths; the error_log copies leak internal paths and stack traces; token.txt is a plaintext token; authorise-application.php is a standalone OAuth-URL emitter that bypasses the framework. The .htaccess also pins the EOL ea-php73 handler.

Evidence
public/info.php and .info.php (repo root) both = <?php phpinfo(); ?> (git-tracked) .htaccess RewriteCond %{REQUEST_FILENAME} !-f serves existing files directly before the index.php front controller, so literal files at root/public are reachable public/error_log (5.9 KB) and root error_log (773 KB) are git-tracked; public/error_log is inside the web root and directly fetchable public/authorise-application.php is a framework-bypassing standalone script that emits a Google Contacts OAuth URL git ls-files confirms .info.php, public/info.php, error_log, public/error_log, token.txt, public/authorise-application.php tracked
Attack scenario
An attacker browses to https://admin.alist.ae/info.php, reads the phpinfo() environment table to harvest leaked .env secrets and exact PHP/Apache versions, fetches public/error_log for paths, and uses authorise-application.php in an OAuth-phishing chain.
Impact

Unauthenticated information disclosure of server internals (phpinfo — often including env-surfaced DB/AWS/Stripe secrets), internal paths/stack traces (error_log), and an OAuth-flow script — directly aiding further compromise and credential reuse.

Remediation
Delete .info.php, public/info.php, root and public error_log, token.txt, and public/authorise-application.php from the repo and server; purge from history; gitignore. Set the document root to /public only. Ensure display_errors=Off and disable directory listing. Remove the ea-php73 handler.
Verifier note

Confirmed: phpinfo content read, public/error_log 5.9KB tracked, .htaccess !-f rewrite serves files; merges raw findings #7 and #38.

AP-30

TrustProxies trusts all proxies (*) enabling X-Forwarded-Host injection and client-IP spoofing

P2 A05:2021 - Security Misconfiguration CWE-290: Authentication Bypass by Spoofing CVSS 6.5 confirmed
Description

TrustProxies sets $proxies='*', so the framework trusts X-Forwarded-Host/Proto/For from ANY client, not just the real load balancer. X-Forwarded-Host can poison Request::getHttpHost()/url() generation (host-header injection, cache/link poisoning), and X-Forwarded-For makes get_client_ip() fully spoofable, allowing rate-limit/IP-block evasion and log forgery.

Evidence
app/Http/Middleware/TrustProxies.php:15 protected $proxies = '*'; TrustProxies.php:22-27 headers include HEADER_X_FORWARDED_HOST and HEADER_X_FORWARDED_FOR app/Helper/helpers.php get_client_ip() returns getClientIps()[1]/[0] — attacker-controlled X-Forwarded-For when proxies='*' get_client_ip is used for access logging and as the key for the admin per-IP rate limit / strike block (config/app.php)
Attack scenario
An attacker brute-forcing the admin login rotates the X-Forwarded-For header on each request; because get_client_ip trusts it, the per-IP strike limiter never triggers.
Impact

Attacker controls the host used in URL generation (cache/redirect poisoning) and can spoof the logged/limited client IP to bypass the admin brute-force rate limiter and corrupt audit logs.

Remediation
Set $proxies to the explicit IP/CIDR of the real load balancer (ALB/CloudFront), not '*'. Do not derive security-sensitive URLs from the request Host; pin to APP_URL. Derive client IP from the trusted proxy's forwarded chain, not the leftmost client-supplied value.
Verifier note

Confirmed: TrustProxies.php:15 proxies='*' with X-Forwarded-Host/For headers enabled.

AP-31

Stripe webhook does not process events; payment success is client-trusted (broken payment workflow integrity)

P2 A04:2021 Insecure Design CWE-840: Business Logic Errors CVSS 6.5 confirmed
Description

The Stripe integration's server-side state transitions are absent/broken. The webhook verifies the signature (with a hardcoded secret) but never acts on the event — it writes a dummy SubscriptionPayment row and returns; the event switch is commented out. The 'payment successful' page is a plain authenticated GET view with no verification that a charge settled, so any entitlement keyed off reaching that page is client-trustable.

Evidence
app/Http/Controllers/SubscriptionController.php:57-65 handleWebhook: after constructEvent it only inserts a SubscriptionPayment with hardcoded placeholders (venue_id=1, stripe_payment_id=1, total_amount=1) and returns 200 — the event switch (82-88) is fully commented out; no subscription/venue is ever activated from a verified event SubscriptionController.php:55 webhook secret hardcoded whsec_9oyth6laBnb2kDZCQdBYVFPW58k9v8iX app/Http/Controllers/SignupController.php paymentSucessful()/paymentFaild() return view('payment_sucessfull')/view('payment_failed') (routes/web.php GET) — reachable without a verified payment
Attack scenario
A merchant navigates directly to the GET payment-success route (or the webhook fires but does nothing); if any activation logic trusts that signal, the merchant gains a paid subscription without a settled charge.
Impact

Depending on how venue subscription activation is wired, a merchant could reach the success state without a verified payment, or paid events silently never reconcile (revenue leakage). The hardcoded whsec_ cannot be rotated via config and is a committed credential.

Remediation
Implement real, idempotent webhook handling: switch on event type (checkout.session.completed, invoice.paid), look up the venue/subscription via event metadata, activate only then, guarded by an idempotency check on the Stripe event id. Never grant entitlement from the success redirect. Move STRIPE_SECRET and STRIPE_WEBHOOK_SECRET to env and rotate the leaked values.
Verifier note

Confirmed: SubscriptionController.php handleWebhook writes dummy rows, switch commented at 82-88; hardcoded whsec_ at :55.

AP-32

Client-trusted redemption identifiers/flags drive privileged offer mutations (IDOR + null-deref DoS)

P2 API1:2023 Broken Object Level Authorization / A04:2021 Insecure Design CWE-639: Authorization Bypass Through User-Controlled Key (with CWE-20, CWE-476) CVSS 6.5 confirmed
Description

Multiple redemption endpoints trust raw client-supplied identifiers and flags without server-side validation or object-level authorization. code_id is dereferenced with ::find(...)->offer_date despite being optional (null-deref DoS), and the same id allocates a voucher with no check that it belongs to the requested offer or was reserved for the requester. companion_id[]/updated_rows[] are attacker-controlled arrays of row ids that get mutated into invitation records. is_dedicated and the unvalidated method field let the client choose code branches.

Evidence
app/Http/Controllers/Api/v1/OfferController.php AcceptOffer validator requires only offer_id and is_dedicated; code_id is optional yet app/Repositories/OfferRepository.php:5098 does FoodOffersUsers::find($requestData['code_id'])->offer_date with NO null check OfferRepository.php:5205-5234 the chosen code_id is assigned user_id=$signup->id with only a block!=null check — no verification the row's offer_id matches the requested offer or was reserved by this user OfferRepository.php:5281-5306 trusts client companion_id[] and updated_rows[] arrays directly (FoodOffersUsers::find($requestData['updated_rows'][$key])->block=2; ->save()) OfferRepository.php:5065 the dedicated-vs-foodoffer path is selected purely by client-supplied is_dedicated
Attack scenario
An attacker POSTs /v1/offers/accept with code_id pointing at another campaign's voucher and updated_rows referencing arbitrary rows, mutating block/user_id on rows they don't own; omitting code_id instead triggers a 500 null-deref DoS.
Impact

Cross-object manipulation: an attacker can target voucher rows from other campaigns by id, force-mutate block/user_id on rows they should not control, and mint invitation rows referencing arbitrary signup ids — corrupting campaign accounting. A missing/invalid code_id reliably triggers an unhandled null dereference (500), a low-effort DoS against the redemption API.

Remediation
Tighten validators: require code_id with exists:food_offers_users,id; bind it with where(offer_id=...) and an ownership/scope check before any mutation; validate method in:[0,1], is_dedicated in:[0,1]. Resolve rows through the authenticated user's own relationships rather than ::find on raw ids. Validate companion_id[]/updated_rows[] as ids belonging to the same offer. Add null guards after every ->find()/->first().
Verifier note

Confirmed: AcceptOffer validator optional code_id; OfferRepository.php ::find($requestData['code_id'])->offer_date with no null check; updated_rows trusted.

AP-33

CSRF disabled on /sc, /sc/* and /send-whatsapp-template; weak SameSite/Secure cookie posture

P2 A01:2021 - Broken Access Control (CSRF) CWE-352: Cross-Site Request Forgery CVSS 5.4 confirmed
Description

VerifyCsrfToken globally excludes the WhatsApp-template send route and the /sc redirect routes from CSRF verification. POST /send-whatsapp-template without CSRF lets an attacker force an authenticated admin's browser to trigger WhatsApp sends (spam/abuse on the org number) via a cross-site auto-submitting form. The /sc POST is public and exempt. Session cookies leave same_site null and do not force Secure.

Evidence
app/Http/Middleware/VerifyCsrfToken.php:21-25 $except = ['/send-whatsapp-template','sc','sc/*'] routes/creator.php sc routes: Route::get('sc/{id?}',...); Route::post('sc',...) — POST /sc is CSRF-exempt and public config/session.php same_site => null; secure => env('SESSION_SECURE_COOKIE', null)
Attack scenario
An admin visits an attacker page that auto-submits a form to /send-whatsapp-template; with CSRF disabled the platform sends attacker-chosen WhatsApp messages from the org's number.
Impact

Cross-site forced actions against authenticated admins (WhatsApp send abuse) and weakened defense-in-depth against CSRF on cookie-authenticated routes.

Remediation
Remove blanket CSRF exemptions; if those routes must be tokenless, gate them behind signed URLs / API-key / signature verification. Set session.same_site to 'lax' (or 'strict') and SESSION_SECURE_COOKIE=true in production.
Verifier note

Confirmed at VerifyCsrfToken.php:21-25 and config/session.php same_site/secure defaults.

AP-34

Open redirect on trusted alist.ae domain via short-link and /sc redirect

P2 A01:2021 - Broken Access Control (Unvalidated Redirect) CWE-601: URL Redirection to Untrusted Site CVSS 5.4 confirmed
Description

The short-link resolver (GET /{code}) 302-redirects to whatever absolute URL was stored as long_link, validated only as a syntactically-valid URL (no domain allowlist). Any authenticated creator can mint a www.alist.ae/<code> link redirecting to an attacker site. /sc redirects (redirect()->away) to original_offer_url similarly without an allowlist. Living on the trusted alist.ae domain, this lends credibility to phishing and can chain into OAuth redirect_uri abuse.

Evidence
app/Http/Controllers/ShortLinkController.php:139 return redirect($find->link); ($find->link validated only 'required|url' at store, no host allowlist) ShortLinkController.php:36-39 $request->validate(['long_link'=>'required|url']) — any absolute URL accepted incl. https://evil.com routes/creator.php Route::get('{code}','ShortLinkController@shortenLink') public GET that 302s to the stored URL app/Http/Controllers/HomeController.php scRedirect uses redirect()->away($redirectURL) where original_offer_url is operator-set from request input without an allowlist
Attack scenario
A creator stores long_link=https://evil.com/phish, distributes the resulting https://www.alist.ae/<code> link; victims trust the brand domain and are silently redirected to the phishing page.
Impact

Phishing amplification under the trusted brand domain, referer-token leakage to attacker hosts, and a redirect primitive usable to bypass URL allowlists in OAuth/SSO flows.

Remediation
Validate redirect targets against an allowlist of trusted hosts before redirecting; reject external hosts or route them through an interstitial warning page. Apply the same allowlist to original_offer_url at write time.
Verifier note

Confirmed: ShortLinkController.php:36-39 validates only url; :139 redirect($find->link).

AP-35

Unauthenticated venue disclosure via Hashids ID (SubscriptionController::getVenue)

P2 API1:2023 Broken Object Level Authorization CWE-639: Authorization Bypass Through User-Controlled Key CVSS 5.3 confirmed
Description

The /v1/venue/{id} endpoint decodes a Hashids-encoded id and returns the entire Venues model unauthenticated, with no access control and no response shaping. Any anonymous caller can enumerate venues and read all stored venue attributes.

Evidence
routes/api_v1.php:27 Route::get('venue/{id}', [SubscriptionController::class, 'getVenue']) declared OUTSIDE the cors/json.response and auth:api groups app/Http/Controllers/SubscriptionController.php:93-101 $venue_id = $hashids->decodeHex($id); $venue = Venues::find($venue_id); return response()->json(['venue' => $venue]) — full Venues record, no auth, no field filtering
Attack scenario
An attacker enumerates Hashids-encoded venue ids against /v1/venue/{id} and scrapes the full Venues records including internal contact details.
Impact

Anonymous disclosure of the full venue catalog and any sensitive venue columns (contact emails/phones, internal flags) by ID enumeration. Confidentiality bounded to venue data, hence Medium.

Remediation
Require auth:api on the route (or move it inside the authenticated group), return an API Resource exposing only public venue fields, and treat Hashids as obfuscation rather than access control.
Verifier note

Confirmed: api_v1.php:27 outside auth group; getVenue returns full model.

AP-36

EOL intervention/image 2.7.2 decoding attacker-controlled images on unauthenticated routes

P2 A06:2021 - Vulnerable and Outdated Components CWE-1104: Use of Unmaintained Third Party Components CVSS 6.5 EPSS low; 2.x EOL, inherits GD/Imagick decoder CVEs on PHP 7.3 confirmed
Description

The application processes attacker-controlled image data through intervention/image v2.7.2, an EOL major superseded by 3.x, a thin wrapper over GD/Imagick that inherits their parser CVEs on the also-EOL PHP 7.3 image extensions. The decode sink is reachable on unauthenticated routes (review/update, offers/save-banner, category/save-image) and public signup flows.

Evidence
composer.lock intervention/image v2.7.2 (2.x line EOL; current major 3.x); runs over the GD/Imagick driver of the unsupported PHP 7.3 runtime routes/api.php:38 review/update registered with NO auth (auth:api commented at api.php:27); UserReviewController feeds up to 5 base64 images to ImageResize::make()->save() routes/api.php:31 offers/save-banner and :43 category/save-image are additional unauthenticated image-upload sinks also reachable from public/creator signup flows (VendorSignupController, SignupController, AutoCompleteController call ImageResize::make on uploaded input)
Attack scenario
An unauthenticated attacker POSTs a malformed/decompression-bomb image to /api/category/save-image; the EOL decoder on PHP 7.3 exhausts memory (DoS) or triggers a decoder memory-corruption bug.
Impact

An unauthenticated attacker can submit crafted image payloads to the decoder, enabling DoS (decompression bombs; memory_limit/post_max_size set to 2056M in .htaccess amplify this) and, worst case, exploitation of native decoder memory-corruption bugs on the unpatched PHP 7.3 GD/Imagick stack. No upstream patch path without upgrading.

Remediation
Upgrade to intervention/image 3.x on a supported PHP 8.2+ runtime with patched GD/Imagick. Add authentication and strict server-side validation (MIME sniffing, max dimensions/pixels/decoded size) before ImageResize::make(). Move heavy image work to a sandboxed/queued worker with resource limits. Add an SCA gate.
Verifier note

Confirmed: composer.lock intervention/image v2.7.2; routes/api.php unauthenticated image sinks.

AP-37

Internal AWS ElastiCache Redis endpoint + socket.io/echo CORS wildcard disclosed in committed laravel-echo-server.json and server.js

P3 A05:2021 - Security Misconfiguration CWE-942: Permissive Cross-domain Policy / CWE-200 Information Exposure CVSS 4.3 confirmed
Description

laravel-echo-server.json (committed despite gitignore) discloses the internal ElastiCache Redis hostname/port and sets a wildcard CORS allowOrigin with allowCors and devMode:true on the realtime broker. server.js sets socket.io CORS origin '*' in production and rebroadcasts events with no auth. ElastiCache Redis often runs without auth, so disclosing the exact endpoint narrows targeting if the Redis port is ever reachable.

Evidence
laravel-echo-server.json:9 host 'alist-redis.tfu6kb.ng.0001.euc1.cache.amazonaws.com' (ElastiCache, eu-central-1); :16 devMode:true; :31-32 allowCors:true, allowOrigin:'*' server.js:23 io = new Server(server,{ cors:{ origin:'*' } }) // 'for development' left in prod (reads letsencrypt certs) .gitignore lists './laravel-echo-server.json' but the file remains git-tracked
Attack scenario
An attacker who finds the SSRF connects to the leaked alist-redis...amazonaws.com endpoint to read/poison queued jobs and session data; separately, any web origin opens socket.io connections due to the wildcard CORS.
Impact

Infrastructure reconnaissance (precise Redis endpoint and region) and cross-origin access to realtime channels/broadcast auth; if the Redis port becomes reachable (e.g. via the SSRF finding) an attacker can read/write cache/queue/session data with no credential. Wildcard CORS allows any origin to open realtime connections.

Remediation
Untrack laravel-echo-server.json and scrub history; drive host/port from env. Restrict socket.io and laravel-echo-server CORS to first-party origins, set devMode:false, and require auth on broadcast events. Ensure ElastiCache requires AUTH and is locked to the app security group.
Verifier note

Confirmed: laravel-echo-server.json:9/16/31-32; server.js:23. Merges raw findings #13 and #37.

AP-38

Predictable OTP/verification-code generation using non-cryptographic mt_rand

P2 A02:2021 Cryptographic Failures CWE-338: Use of Cryptographically Weak PRNG CVSS 5.3 confirmed
Description

The primary OTP/PIN generators use mt_rand(), a non-cryptographic Mersenne Twister PRNG whose output is predictable: with a few observed outputs an attacker can recover internal state and predict subsequent values. For short-lived numeric login OTPs and static venue PINs this compounds the small-keyspace and no-lockout issues.

Evidence
app/Repositories/SignupRepository.php:900-902 generateNDigitRandomNumber($length) { return mt_rand(pow(10,$length-1), pow(10,$length)-1); } app/Helper/helpers.php generateNDigitRandomNumberOtp uses mt_rand used for login OTPs (SignupRepository.php:2215) and venue PINs (VenuesController.php:1662) some sibling code uses random_int (SignupRepository.php:286/1327) — inconsistent, but the primary login OTP/PIN generators use mt_rand
Attack scenario
An attacker repeatedly requests OTPs for their own account to sample mt_rand outputs, recovers PRNG state, then predicts the OTP issued to a victim account in the same window.
Impact

An attacker able to observe a small number of generated OTPs (e.g. by requesting OTPs for attacker-controlled accounts) may predict OTPs issued to victim accounts, reducing or eliminating brute-force effort.

Remediation
Replace all mt_rand-based OTP/code generation with random_int() (CSPRNG) or Str::random for token material. Standardize on a single secure OTP helper and increase length to >=6 digits.
Verifier note

Confirmed at SignupRepository.php:900-902 and VenuesController.php:1662-1666.

AP-39

Committed error_log (758 KB) leaks internal filesystem paths and hosting account names

P2 A09:2021 - Security Logging and Monitoring Failures CWE-532: Insertion of Sensitive Information into Log File CVSS 5.3 confirmed
Description

A 774 KB Apache/PHP error_log is committed at the repo root (plus a copy under public/). It contains accumulated errors and stack traces revealing internal absolute paths and cPanel account usernames (parhaman, frinkae). The public/ copy is web-fetchable; even in-repo it corroborates the DB username and aids targeting.

Evidence
error_log — 4001 lines, ~774 KB, committed at repo root and git-tracked (wc -l confirms 4001; ls shows 773945 bytes) leaked absolute paths /home/parhaman/ and /home2/frinkae/ (current and prior cPanel accounts), exposing username 'parhaman' which matches the hard-coded DB user parhaman_admin a smaller copy exists at public/error_log (web-fetchable)
Attack scenario
An attacker fetches public/error_log (or reads the repo), maps the server filesystem layout and confirms the DB username parhaman_admin, then combines it with the hard-coded DB password to target the database.
Impact

Information disclosure: internal directory layout, hosting account names, library paths, and error context aiding path-traversal/LFI targeting and credential correlation (the 'parhaman' account name matches the hard-coded DB username).

Remediation
Delete error_log and public/error_log from the repo and history; gitignore *.log. Keep production logs outside the web root and never commit them. Rotate the DB credentials whose username appears here.
Verifier note

Confirmed: wc -l=4001, size 773945; public/error_log tracked.

AP-40

Google OAuth token-refresh response (access/refresh token) written to application logs

P2 A09:2021 - Security Logging and Monitoring Failures CWE-532: Insertion of Sensitive Information into Log File CVSS 4.4 confirmed
Description

GoogleContactsService logs the full JSON of the OAuth token-refresh response at INFO level with a '// Debugging' comment (leftover debug code). The response contains a live bearer access_token (and potentially refresh/id tokens), landing in Laravel logs in plaintext on every contacts sync.

Evidence
app/Services/GoogleContactsService.php:58 Log::info("Google Token Response: " . json_encode($newAccessToken)); // Debugging context lines 56-66: $newAccessToken is the raw array from fetchAccessTokenWithRefreshToken(), containing access_token (and possibly refresh/id tokens)
Attack scenario
An attacker with read access to storage/logs greps for 'Google Token Response', extracts the access_token, and calls the People API to read contacts before it expires.
Impact

Anyone able to read application logs (ops staff, an attacker exploiting the committed error_log / log exposure, or limited server access) obtains valid Google API bearer tokens, enabling access to the linked account's contacts for the token lifetime (and persistently if a refresh token is logged).

Remediation
Remove the Log::info call (or log only success/failure, never the token). Audit existing logs for captured tokens and rotate the affected Google refresh token. Add a lint rule prohibiting logging of token/secret/password variables.
Verifier note

Confirmed at GoogleContactsService.php:58.

AP-41

Over-permissive mass-assignment guard on Signup model exposes trust/privilege fields

P2 A04:2021 - Insecure Design CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes CVSS 5.0 likely
Description

The Signup (creator) model whitelists security-sensitive attributes — is_approved, is_verified, instagram_verified, tier, api_token — for mass assignment. The current API profile-update repository assigns fields explicitly and does not expose them today, but the broad $fillable is a latent privilege-escalation primitive: any code path that mass-assigns request input into Signup would let a creator self-approve, change tier, or overwrite api_token. Given the codebase's pattern of passing $request->all() into repositories, this is a meaningful design weakness.

Evidence
app/Signup.php:28 $fillable = ['name','email',...,'is_approved','tier','is_verified','instagram_verified','api_token','device_token',...] — approval/trust/auth-token fields are mass-assignable No Signup::create($request->all())/->update($request->all()) sink was found in app/Repositories or app/Http/Controllers/Api at audit time (grep returned none), so exploitability depends on other/future call sites; the API profile-update path assigns fields individually
Attack scenario
A future or overlooked endpoint does Signup::update($request->all()); a creator includes is_approved=1 and tier=top in the body and self-approves to a higher tier.
Impact

If any creator-reachable endpoint mass-assigns request input to a Signup, an attacker could self-approve their account, change tier, or overwrite their API token, bypassing the admin approval workflow.

Remediation
Remove is_approved, is_verified, instagram_verified, tier, and api_token from $fillable (set them only via explicit server-side logic). Audit all Signup write paths and add a regression test asserting a creator cannot set is_approved/tier via the API.
Verifier note

Confirmed $fillable at Signup.php:28 includes the sensitive fields; no current mass-assign sink found, hence 'likely'.

AP-42

Plaintext token.txt committed at repository root

P3 A02:2021 - Cryptographic Failures CWE-312: Cleartext Storage of Sensitive Information CVSS 3.7 EPSS n/a (credential leak, not a CVE) confirmed
Description

A file named token.txt containing a 20-character high-entropy string is committed at the repo root. The name and entropy indicate a credential/API token. No code references it, so it is likely a leftover artifact, but it remains a cleartext secret in the repo and history. (Also bundled into AP-08 as part of the OAuth-credential exposure; tracked here for completeness as an unreferenced artifact.)

Evidence
token.txt — single line 'fALezGLFHvHGNWBZtTTV' (20 chars, high-entropy); git-tracked; introduced in commit 537082e40 no code references token.txt (grep)
Attack scenario
An attacker scanning the repo finds token.txt and replays the value against likely services (Instagram/TikTok/Modash/internal APIs) to test for an active session or token.
Impact

Cleartext secret distributed in every clone and in history. If it corresponds to a live integration it can be replayed; even if dead it is poor hygiene and may correlate with other systems.

Remediation
Identify which service the token belongs to, revoke it, delete token.txt from tree and history, and gitignore stray token artifacts. Confirm no integration reads it before removal.
Verifier note

Confirmed token.txt content; unreferenced. Overlaps AP-08.

AP-43

Weak SHA-1/MD5 used as integrity/obfuscation tokens for offer access and upload filenames

P3 A02:2021 - Cryptographic Failures CWE-328: Use of Weak Hash CVSS 3.1 confirmed
Description

SHA-1 and MD5 are used to derive date-encoded tokens that gate offer eligibility and to name uploaded files. sha1($date) is deterministic over a tiny input space, so any if (sha1($checkDate) != $todayDate) check is not a security control — the value is fully predictable. md5(time()) for filenames is predictable to the second, enabling guessing/overwrite races. Not used for password storage (Laravel bcrypt handles that), so impact is limited.

Evidence
app/Http/Controllers/VenueOfferController.php:1516 if (sha1($checkDate) != $todayDate) — sha1 of a date used as a freshness/access check app/Http/Controllers/OfferController.php:2376 $date_enc = sha1($selectDate); app/Http/Controllers/VendorSignupController.php:205 $fileName = md5(time()) . ".$ext"; (predictable filename) additional sha1($date) in Admin/FoodOffersController.php and NewsLetterController.php
Attack scenario
An attacker computes sha1(today's date) and submits it as the expected token to a venue-offer endpoint, satisfying the check without a server-issued value; and guesses md5(time()) filenames to access/overwrite other users' uploads.
Impact

Any access decision relying on matching sha1(date) is bypassable since the attacker computes the same hash; predictable md5(time()) filenames allow enumeration/overwrite of uploaded assets. Low overall impact (gates offer freshness, not authentication).

Remediation
Do not hash low-entropy/known inputs as security tokens. Use a keyed HMAC (hash_hmac('sha256', $date, APP_KEY)) or Laravel signed URLs for tamper-evident date tokens. Use Str::random()/Str::uuid() for unpredictable filenames. Migrate remaining SHA-1/MD5 integrity uses to SHA-256.
Verifier note

Confirmed at VenueOfferController.php:1516, OfferController.php:2376, VendorSignupController.php:205.

AP-44

Passport bearer token placed in browser cookie without enforced Secure/SameSite flags

P3 A05:2021 - Security Misconfiguration CWE-614/1275: Sensitive Cookie Without Secure / Improper SameSite CVSS 4.8 confirmed
Description

The creator portal stores the Passport access token in the alistToken cookie via Cookie::make with only name/value/minutes, leaving Secure false and SameSite null; session.php also leaves secure and same_site null. EncryptCookies encrypts the body and httpOnly defaults true, but the absence of an enforced Secure attribute permits transmission over plain HTTP, and unset SameSite weakens cross-site posture for the credential cookie.

Evidence
app/Http/Controllers/CreatorController.php:241 Cookie::queue(Cookie::make('alistToken', $token, config('session.lifetime'))); — 3-arg form leaves $secure=false, $sameSite=null config/session.php secure => env('SESSION_SECURE_COOKIE', null); same_site => null app/Http/Middleware/TokenAuthentication.php:21-22 re-injects the alistToken cookie value as Authorization: Bearer — the cookie IS the API credential Mitigating: the creator group includes EncryptCookies, so the cookie body is encrypted at rest
Attack scenario
On any accidental HTTP request to the creator domain, the non-Secure alistToken cookie is transmitted in cleartext and intercepted by a MITM, who replays it.
Impact

Increased risk of token-cookie exposure over cleartext transport (MITM on any HTTP path) and weaker cross-site protections for the credential cookie. Lower severity because EncryptCookies and httpOnly reduce direct theft/XSS readability.

Remediation
Explicitly set Secure=true and SameSite='Lax'/'Strict' on the alistToken cookie; set SESSION_SECURE_COOKIE=true and same_site='lax'. Force HTTPS-only (HSTS). Prefer sending the bearer token via the Authorization header rather than a cookie.
Verifier note

Confirmed: CreatorController.php:241 3-arg Cookie::make; TokenAuthentication.php re-injects cookie as Bearer.

AP-45

Missing HTTP security headers and broken X-Frame-Options (clickjacking / MIME-sniffing exposure)

P3 A05:2021 - Security Misconfiguration CWE-1021: Improper Restriction of Rendered UI Layers / CWE-693 CVSS 4.3 confirmed
Description

The application sets no HSTS, CSP, X-Content-Type-Options, Referrer-Policy, or Permissions-Policy. The only frame-control header uses the obsolete X-Frame-Options: ALLOW-FROM directive, which modern browsers ignore — so the admin/web app has no effective clickjacking protection and no CSP to mitigate XSS or restrict frame ancestors.

Evidence
app/Http/Middleware/Cors.php:23 $response->headers->set('X-Frame-Options','ALLOW-FROM https://partners.alist.ae'); — ALLOW-FROM is deprecated and ignored by modern browsers => no frame protection grep across app/, config/, .htaccess: no Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options, Referrer-Policy, or Permissions-Policy config/session.php cookies not forced Secure and no SameSite, compounding the lack of HSTS
Attack scenario
An attacker frames an admin action page on attacker.com; because X-Frame-Options ALLOW-FROM is ignored and no CSP frame-ancestors exists, the admin is clickjacked into performing a privileged action.
Impact

Clickjacking of admin actions, increased XSS impact (no CSP), MIME-sniffing, and lack of strict transport security across the admin/creator/business portals.

Remediation
Add a global security-headers middleware: Strict-Transport-Security (max-age>=31536000; includeSubDomains), CSP with frame-ancestors, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, and replace X-Frame-Options: ALLOW-FROM with SAMEORIGIN (use CSP frame-ancestors for the partners.alist.ae allowance).
Verifier note

Confirmed at Cors.php:23; no other security headers found.

AP-46

Mass assignment via create($request->all()) on ChatAgentShortCut and emailTemplate

P3 A04:2021 - Insecure Design CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes CVSS 3.5 confirmed
Description

Two admin endpoints pass $request->all() into Eloquent create(), relying entirely on the model $fillable allowlist to block extra fields. Both models define tight $fillable allowlists that exclude sensitive columns, so this is not currently exploitable for escalation. Flagged as code-hygiene / latent risk: a future migration adding a sensitive column without updating $fillable, or loosening to $guarded=[], makes these live mass-assignment vulnerabilities.

Evidence
app/Http/Controllers/Admin/UserController.php:777 $data = ChatAgentShortCut::create($request->all()); (model $fillable = ['title','description'] — app/ChatAgentShortCut.php:16) app/Http/Controllers/Admin/EmailTemplateController.php:51 emailTemplate::create($request->all()); (model $fillable = ['title','text','template_design_id'] — app/emailTemplate.php:9)
Attack scenario
A future migration adds a sensitive column to email_templates without updating $fillable; an authenticated admin then sets it via the unfiltered create($request->all()).
Impact

No current privilege/data escalation (allowlist holds). Latent risk of future mass-assignment if model guards are broadened or sensitive columns are added.

Remediation
Replace create($request->all()) with $request->validate([...]) then create($request->only([...])). Keep $fillable tight and never switch these models to $guarded = [].
Verifier note

Confirmed: UserController.php:777, EmailTemplateController.php:51; both models have tight $fillable.

AP-47

EOL/abandoned PHP dependencies: stripe-php v10, predis 1.1.10, fzaninotto/faker (abandoned), phpspreadsheet 1.30.2

P3 A06:2021 - Vulnerable and Outdated Components CWE-1104: Use of Unmaintained Third Party Components CVSS 3.7 EPSS low; hygiene/unmaintained risk confirmed
Description

Several PHP dependencies are on unmaintained/abandoned versions: stripe-php held at EOL v10 (with a server/client SDK skew vs Stripe.js v12), fzaninotto/faker explicitly abandoned and pinned to 1.5.0, predis on EOL 1.x (though phpredis is the configured runtime client), and phpspreadsheet on EOL 1.x (export-only so parser CVEs are not currently reachable). These are unmaintained-component/hygiene risks rather than directly exploitable today.

Evidence
composer.lock stripe/stripe-php v10.21.0 (v10 EOL; current v16+); JS side uses stripe ^12.5.0 (SDK skew) composer.lock predis/predis v1.1.10 (1.x EOL; current 2.x). Mitigated: config/database.php sets REDIS_CLIENT default 'phpredis' composer.lock fzaninotto/faker v1.5.0 flagged abandoned:true (successor fakerphp/faker); require-dev composer.lock phpoffice/phpspreadsheet 1.30.2 (1.x EOL) shipped via maatwebsite/excel; usage is export-only, so read-side XXE/formula CVEs are largely unreachable today
Attack scenario
A newly disclosed CVE in EOL stripe-php v10 (e.g. response-parsing) goes unpatched because no SCA gate exists and the major is unmaintained, exposing the payments path over time.
Impact

Hygiene/unmaintained risk. The stripe-php v10 EOL means payment-SDK security fixes and API-deprecation handling are not received, which becomes material over time for a payments platform; the others broaden the trusted dependency footprint with no security fixes.

Remediation
Upgrade stripe/stripe-php to a supported major aligned with Stripe.js; replace fzaninotto/faker with fakerphp/faker (require-dev); migrate predis to 2.x or standardize on phpredis and drop predis; upgrade maatwebsite/excel to a release depending on phpspreadsheet 2.x/3.x. Gate behind a CI SCA + composer audit check.
Verifier note

Confirmed versions in composer.lock: stripe v10.21.0, predis v1.1.10, faker v1.5.0, phpspreadsheet 1.30.2.

AP-48

End-of-life client-side stack: Vue 2.7, Bootstrap 4, axios 0.21.4, socket.io-client 2.5.0, jQuery, laravel-mix 6

P3 A06:2021 - Vulnerable and Outdated Components CWE-1104: Use of Unmaintained Third Party Components CVSS 4.3 EPSS low; EOL frontend, latent client-side CVE accrual confirmed
Description

The entire frontend build chain is EOL: Vue 2 (no patches since 2023-12-31), Bootstrap 4, axios 0.x, Socket.IO v2 client, and laravel-mix 6. axios 0.21.x predates the 1.x security line that fixed SSRF and X-CSRF-token-leak issues. With no SCA gate, none are flagged for upgrade. webpack.mix.js also leaves source maps enabled so a production build can ship original source.

Evidence
package-lock.json axios 0.21.4 (0.x EOL; predates the 1.x SSRF/CSRF-token-leak fixes), socket.io-client 2.5.0 (v2 EOL), vue 2.7.14 (Vue 2 EOL 2023-12-31), bootstrap 4.6.2, jquery 3.7.0, laravel-mix 6.0.49 package.json declares the EOL toolchain webpack.mix.js enables .sourceMaps()/inline-source-map — a production build would emit source maps exposing original JS
Attack scenario
A future DOM-XSS CVE in an EOL frontend dependency is published with no patch available; because there is no SCA gate, the vulnerable bundle stays in production.
Impact

EOL client libraries will accrue unpatched DOM-XSS, prototype-pollution, and request-handling CVEs with no upstream fixes, increasing the long-term client-side attack surface of the admin/creator portals. Inline source maps in production leak original JS to visitors, aiding recon.

Remediation
Plan migration off the EOL stack (Vue 2->3, Bootstrap 4->5, axios->1.x, socket.io-client->4.x, laravel-mix->Vite). Remove .sourceMaps()/inline-source-map from production builds. Add npm audit/Snyk to CI. Delete the orphan package-del-lock.json.
Verifier note

Confirmed via package-lock.json/package.json versions and webpack.mix.js sourceMaps.

AP-49

No CI/CD pipeline and no dependency scanning — manual FTP deploys with zero supply-chain integrity controls

P2 A06:2021 - Vulnerable and Outdated Components CWE-1104: Use of Unmaintained Third Party Components CVSS 5.9 confirmed
Description

The platform has no CI/CD configuration. Deployment appears to be manual FTP (evidenced by .ftpquota and package-del-lock.json), so dependencies are shipped without SCA, lockfile-integrity verification, or build provenance. There is no composer audit/npm audit gate, no SBOM, and no pinned-toolchain build, so newly disclosed CVEs in the many outdated deps are never surfaced and a tampered vendor dir/lockfile could deploy undetected.

Evidence
No .github/workflows, .gitlab-ci.yml, Jenkinsfile, .circleci, or bitbucket-pipelines.yml in the tree (only .styleci.yml and a vendored Date-Time-Picker .travis.yml) git remote origin git@bitbucket.org:ParhamandCo/alist-portal.git with no pipeline file repo root contains .ftpquota and an orphan package-del-lock.json (354 KB) — manual FTP deploys no .snyk, no composer/npm audit gating, no SBOM, and zero real tests (only ExampleTest stubs)
Attack scenario
A dependency is compromised upstream (typosquat or maintainer takeover); with no SCA gate the malicious version is pulled and FTP-deployed to production undetected.
Impact

Absence of any SCA/build gate means the numerous outdated/EOL dependencies are never flagged or upgraded, and any future supply-chain compromise reaches production unchecked. Manual FTP deploys defeat reproducibility/provenance.

Remediation
Introduce a pipeline that on every push runs composer audit, npm audit, and an SCA scan with a severity gate, and generates a CycloneDX SBOM. Replace manual FTP with a scripted version-pinned deploy (composer install --no-dev --optimize-autoloader, npm ci). Remove package-del-lock.json and stop committing .ftpquota. Pin CI actions to SHAs with least-privilege tokens.
Verifier note

Confirmed: no pipeline files in tree; .ftpquota and package-del-lock.json present; only ExampleTest stubs.

Remediation roadmap

Immediate
Rotate-before-refactor: every committed secret is compromised the moment it touched the Bitbucket repo. Treat all as live and rotate first, then remove from history.
  • Revoke + reissue the Firebase Admin service-account key (id d07b7e1be998da23d756303beeb0bf7bbdb96e88) — AP-01.
  • Revoke all Slack webhooks, the Monday.com me:write tokens, and both Google OAuth client_secrets + refresh tokens (the GOCSPX-- secret is shared across thirdparty.php and googlePeople.json — rotate once, update all consumers) — AP-07, AP-08.
  • Rotate the MySQL password !0325Parham!, the Stripe sk_test/whsec_ values, the token.txt credential, and the alist-dev-aswin.pem keypair — AP-26, AP-27, AP-28, AP-42.
  • Remove the 'otp' => $otp field from the login response and delete the subin@alist.ae / 1122 backdoor — AP-05, AP-21.
  • Move creator/otpshow/linkshow inside the auth group and the ~39 unauthenticated admin routes inside ['auth','permission'] — AP-03, AP-06.
  • Delete .info.php, public/info.php, error_log, public/error_log, token.txt, public/authorise-application.php and purge from history (filter-repo/BFG) — AP-29, AP-39.
This week
Close the unauthenticated-takeover and injection paths.
  • Remove the if ($request->ajax()) return $next($request) permission bypass; evaluate ->can() for all requests — AP-02.
  • Add per-account + per-IP OTP lockout with backoff, raise OTP to >=6 digits via random_int(), and apply throttle:5,1 to all OTP routes — AP-04, AP-38.
  • Parameterize every confirmed SQLi sink (filterPreviews, listCategoryVenues, venueDropListAuto, userActionHistory, getReviewWidget) and integer-cast/validate stored avail_days — AP-09 to AP-14.
  • Apply auth:api across routes/api.php, allowlist the dynamic review-column write, and scope notification delete to the owner — AP-15, AP-17.
  • Move the pre-auth creator object-ID routes inside authenticateToken:creator with ownership checks — AP-16.
  • Replace the wildcard CORS middleware with Laravel HandleCors + a first-party origin allowlist — AP-18.
  • Validate image inputs as multipart uploads; block SSRF (scheme/host allowlist, RFC1918/IMDS deny) before any file_get_contents — AP-19.
This month
Fix the runtime, the money paths, and authorization design.
  • Upgrade the cPanel handler to PHP 8.2/8.3 and run composer/build on the serving PHP — AP-20.
  • Hash PINs/OTPs (bcrypt/argon2, constant-time compare), set Passport token expiry + a /v1/logout revocation route — AP-22, AP-23.
  • Wrap redemption in DB::transaction() + lockForUpdate() (or conditional UPDATE), replace static venue PINs with single-use CSPRNG tokens, and tighten redemption-id validators — AP-24, AP-25, AP-32.
  • Implement real idempotent Stripe webhook handling and stop trusting the success redirect for entitlement — AP-31.
  • Require auth on /v1/venue/{id}, pin TrustProxies to the load balancer, remove the CSRF exemptions, and allowlist redirect targets — AP-30, AP-33, AP-34, AP-35.
Hardening
Defense-in-depth and supply-chain controls so regressions surface automatically.
  • Add a global security-headers middleware (HSTS, CSP frame-ancestors, nosniff, Referrer-Policy) and replace the obsolete X-Frame-Options: ALLOW-FROM; set Secure/SameSite on the alistToken and session cookies — AP-44, AP-45.
  • Upgrade EOL dependencies (intervention/image 3.x, stripe-php, predis/phpredis, fakerphp/faker, phpspreadsheet) and the EOL frontend stack; remove production source maps — AP-36, AP-47, AP-48.
  • Tighten the Signup $fillable and replace create($request->all()) with validated only() — AP-41, AP-46.
  • Replace SHA-1/MD5 integrity tokens with keyed HMAC / signed URLs and CSPRNG filenames; lock down the echo-server/socket.io CORS and Redis exposure — AP-37, AP-43.
  • Stand up a CI/CD pipeline with gitleaks/trufflehog secret scanning, composer audit + npm audit SCA gates, an SBOM, and a scripted version-pinned deploy replacing manual FTP — AP-40, AP-49.

Code quality

Run / deploy

Local setup

# 1. Install dependencies (don't run during audit; use ops box)
composer install
npm ci

# 2. Environment — .env.example is NOT in the repo
#    Copy a sanitised .env from ops, then:
php artisan key:generate
php artisan passport:install      # writes storage/keys/{oauth-private,oauth-public}.key

# 3. Database
php artisan migrate               # 381 migrations
php artisan db:seed               # countries, currencies, email templates, account settings

# 4. Assets
npm run prod                      # via laravel-mix

# 5. Run
php artisan serve                 # or point Nginx/Apache at /public
php artisan queue:work redis      # required for jobs (FCM push, mailers, broadcasts, analytics)
php artisan schedule:work         # or system cron: * * * * * php artisan schedule:run

# 6. Realtime (only if testing sockets locally)
laravel-echo-server start         # reads laravel-echo-server.json
node server.js                    # requires letsencrypt certs — prod only

Environment

Deployment hints

Origin is Bitbucket (bitbucket.org:ParhamandCo/alist-portal). There is no CI config (no bitbucket-pipelines.yml, no Dockerfile). Hosted on cPanel — .htaccess contains cPanel-generated handlers and an .ftpquota file is committed, strongly suggesting FTP-based deploys. The document root is the repo root, and .htaccess rewrites every request to /public/$1 only when the requested file does not already exist (a RewriteCond !-f trap — literal files at root/public are served directly). Production domains observed: alist.ae, www.alist.ae, admin.alist.ae, development-admin.alist.ae, alist-admin.qubicle.net. Cron is required for the scheduler (every minute). Redis is AWS ElastiCache in eu-central-1 — see laravel-echo-server.json.

What to know before editing