Laravel 11 monolith powering the entire A-List platform: admin/operations panel, vendor and creator portals, and the mobile-app REST API.
admin.alist.ae), the vendor (merchant/brand) portal, the creator (influencer) signup web flows, and the JSON REST API consumed by the iOS/Android apps. UAE-based influencer-marketing SaaS./v1/* in routes/api_v1.php (336 LOC, mobile contract), plus an older /api/* and a large admin web surface in routes/admin.php (832 LOC).| Category | Technology | Version | Notes |
|---|---|---|---|
| Language | PHP | ^8.2 | composer.json — but .htaccess still pins ea-php73 handler. Verify live runtime. |
| Framework | Laravel | v11.48.0 | Recent. Migrated up from older Laravel (legacy patterns linger). |
| Web auth | Session + Spatie Permission | 6.24.1 | Custom PermissionMiddleware with hardcoded route exceptions. |
| API auth | Laravel Passport | v13.4.4 | OAuth2 against signups provider (creator users). |
| DB | MySQL via Eloquent + doctrine/dbal | 3.x | 381 migrations. Hardcoded TZ Asia/Dubai. |
| Cache/Queue/Broadcast | Redis | predis 1.1.10 | AWS ElastiCache host leaked in laravel-echo-server.json. |
| Realtime | Pusher + laravel-echo-server (Redis) + bespoke server.js socket.io | — | Echo on :6001, custom socket on :8081 (CORS origin: "*"). |
| Payments | stripe/stripe-php | v10.21.0 | Subscriptions + webhooks via SubscriptionController. |
| Laravel Mail + jdavidbakr/mail-tracker | 7.x | Tracked opens/clicks; mail-tracker stores all sent mail in DB. | |
| Error tracking | sentry/sentry-laravel | 4.20.1 | Wired in config/app.php providers. |
| PDF / Excel | barryvdh/laravel-dompdf 3.0.0, maatwebsite/excel 3.1.58 | — | Review-archive PDFs + admin exports. |
| Images | intervention/image 2.7.2, league/flysystem-aws-s3-v3 3.0 | — | S3 uploads. |
| 3rd-party | Google APIs, FCM, Modash, TikTok, Interakt (WhatsApp), Stevebauman geo-IP | — | Many of these have credentials currently committed. |
| Frontend | Blade + jQuery + Bootstrap 4 + Vue 2.5 | Laravel Mix 6 | Mostly server-rendered admin pages. |
| Tests | PHPUnit | 11.x | Only stub ExampleTest.php files — effectively zero coverage. |
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.
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.
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
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.
| Category | Status | Notes |
|---|---|---|
| A01:2021 — Broken Access Control | vuln | AJAX 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 Failures | vuln | Firebase 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 — Injection | vuln | Four 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 Design | vuln | Non-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 Misconfiguration | vuln | Wildcard 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 Components | vuln | EOL 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 Failures | vuln | 4-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 Failures | partial | No 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 Failures | vuln | OAuth 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) | vuln | file_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) | vuln | API1 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) | vuln | This 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. |
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.
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.
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.Confirmed by reading storage/fcm.json, config/fcm.php, and pushNotification.php:37; key is verbatim and runtime-loaded.
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.
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.
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.Confirmed at PermissionMiddleware.php:110-117; the AJAX branch is the only path that skips ->can().
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.
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.
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.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.
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.
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.
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.Confirmed: SignupRepository.php:2255 lookup, mt_rand 9000-keyspace, login routes have no throttle override; only global throttle:500,1.
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.
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.
'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.Confirmed at SignupRepository.php:2236 — 'otp' => $otp returned with TODO comment.
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.
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.
$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.Confirmed: route at creator.php:35 precedes auth group at :91; otpShow returns authenticationRandomNumber with no auth/ownership check.
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.
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.
git rm --cached config/thirdparty.php, scrub history, and verify the gitignore now excludes the untracked file.Confirmed: file is git-tracked despite .gitignore; production Slack/Monday/Google secrets read verbatim from config/thirdparty.php.
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.)
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.
Confirmed by reading .config.json, storage/googlePeople.json, GoogleAuthController.php:14, token.txt. Merges raw findings #3, #10, and #34 (auth_jwt duplicate).
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.
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.
(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.Confirmed at UsersPreviewsController.php:208/237/269/275/279; all four inputs concatenated into DB::select with no bindings.
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.
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.
$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)->...Confirmed at AuditController.php:106/110.
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.
Full read/modify of the MySQL database for any authenticated back-office user; potential FILE-privilege escalation to RCE.
DB::select("... WHERE venue_title LIKE ? ...", ['%'.$seacrh.'%']); or the Query Builder where('venue_title','LIKE',"%$seacrh%").Confirmed at FoodOffersController.php:2243/2245.
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.
Authenticated back-office user can read/alter the entire database via injection; potential RCE via FILE privilege.
$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.Confirmed at AdminController.php:3203/3204.
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.
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.
? placeholders passed to DB::select, or use the Query Builder whereBetween('O.offer_date',[$startDate,$endDate]). Validate startDate/endDate with the 'date' rule.Confirmed at DashboardController.php:1078/1082/1087/1092.
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.)
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.
'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.Confirmed: VenuesController.php:729 stores raw input; app/Venues.php:14 fillable commented; OfferRepository.php:6218 interpolates avail_days into whereRaw.
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.
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.
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.Confirmed: routes/api.php has no auth, reviewUpdateAPI uses ::find($request->review_id) and dynamic $review->$field_name with no ownership check.
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.
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.
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.Confirmed: creator.php:59-90 routes precede the auth group at :91; creator middleware group has no auth.
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.
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.
Notification::where('id',$requestData['id'])->where('user_id',auth()->id())->first()->delete(); return not-found when zero rows match.Confirmed: NotificationRepository.php:51 ::find with no user_id scope vs all() at :38 which does scope.
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).
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.
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.Confirmed: Cors.php:20 wildcard; Kernel.php:24 global; HandleCors not registered.
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.
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.
$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.Confirmed: UserReviewController.php:168 and SignupController.php:709 pass $request->imageN to file_get_contents with no validation.
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.
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.
composer install --no-dev on the same PHP that serves traffic. Add a php -v deploy gate enforcing >=8.2.Confirmed: .htaccess:110 ea-php73 handler; composer.lock laravel/framework v11.48.0 requires php >=8.2.
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.
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.
email==literal / otp==literal shortcuts.Confirmed at SignupRepository.php:2212-2214.
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.
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.
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.Confirmed: SignupRepository.php:1120/2218/2731 plaintext+loose==; Signup.php:35 hidden lacks pin/otp.
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).
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).
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.Confirmed: AuthServiceProvider boot has no expiry override; no /logout in api_v1.php; SESSION_LIFETIME default 43200.
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.
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.
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.Confirmed: OfferRepository.php:7542 compares venu_code; generateNDigitRandomNumber 4-digit; no lockout in verifyVenueCode.
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.
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.
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.Confirmed: OfferRepository.php:5057 transaction commented; fetchAndLockOfferCode 4185-4216 has no lock; verifyVenueCode used_at TOCTOU.
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.
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.
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.Confirmed at config/database.php:52-53.
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.
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.
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.Confirmed at StripeController.php:20, SubscriptionController.php:42/55.
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.
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.
filter-repo/BFG) and force-push. Confirm no other *.pem exists in history.Confirmed by git show 6f6b81d93:alist-dev-aswin.pem returning the RSA key.
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.
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.
/public only. Ensure display_errors=Off and disable directory listing. Remove the ea-php73 handler.Confirmed: phpinfo content read, public/error_log 5.9KB tracked, .htaccess !-f rewrite serves files; merges raw findings #7 and #38.
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.
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.
$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.Confirmed: TrustProxies.php:15 proxies='*' with X-Forwarded-Host/For headers enabled.
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.
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.
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.Confirmed: SubscriptionController.php handleWebhook writes dummy rows, switch commented at 82-88; hardcoded whsec_ at :55.
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.
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.
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().Confirmed: AcceptOffer validator optional code_id; OfferRepository.php ::find($requestData['code_id'])->offer_date with no null check; updated_rows trusted.
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.
Cross-site forced actions against authenticated admins (WhatsApp send abuse) and weakened defense-in-depth against CSRF on cookie-authenticated routes.
session.same_site to 'lax' (or 'strict') and SESSION_SECURE_COOKIE=true in production.Confirmed at VerifyCsrfToken.php:21-25 and config/session.php same_site/secure defaults.
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.
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.
Confirmed: ShortLinkController.php:36-39 validates only url; :139 redirect($find->link).
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.
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.
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.Confirmed: api_v1.php:27 outside auth group; getVenue returns full model.
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.
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.
ImageResize::make(). Move heavy image work to a sandboxed/queued worker with resource limits. Add an SCA gate.Confirmed: composer.lock intervention/image v2.7.2; routes/api.php unauthenticated image sinks.
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.
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.
devMode:false, and require auth on broadcast events. Ensure ElastiCache requires AUTH and is locked to the app security group.Confirmed: laravel-echo-server.json:9/16/31-32; server.js:23. Merges raw findings #13 and #37.
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.
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.
random_int() (CSPRNG) or Str::random for token material. Standardize on a single secure OTP helper and increase length to >=6 digits.Confirmed at SignupRepository.php:900-902 and VenuesController.php:1662-1666.
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.
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).
*.log. Keep production logs outside the web root and never commit them. Rotate the DB credentials whose username appears here.Confirmed: wc -l=4001, size 773945; public/error_log tracked.
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.
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).
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.Confirmed at GoogleContactsService.php:58.
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.
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.
Confirmed $fillable at Signup.php:28 includes the sensitive fields; no current mass-assign sink found, hence 'likely'.
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.)
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.
Confirmed token.txt content; unreferenced. Overlaps AP-08.
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.
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).
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.Confirmed at VenueOfferController.php:1516, OfferController.php:2376, VendorSignupController.php:205.
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.
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.
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.Confirmed: CreatorController.php:241 3-arg Cookie::make; TokenAuthentication.php re-injects cookie as Bearer.
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.
Clickjacking of admin actions, increased XSS impact (no CSP), MIME-sniffing, and lack of strict transport security across the admin/creator/business portals.
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).Confirmed at Cors.php:23; no other security headers found.
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.
No current privilege/data escalation (allowlist holds). Latent risk of future mass-assignment if model guards are broadened or sensitive columns are added.
create($request->all()) with $request->validate([...]) then create($request->only([...])). Keep $fillable tight and never switch these models to $guarded = [].Confirmed: UserController.php:777, EmailTemplateController.php:51; both models have tight $fillable.
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.
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.
composer audit check.Confirmed versions in composer.lock: stripe v10.21.0, predis v1.1.10, faker v1.5.0, phpspreadsheet 1.30.2.
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.
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.
.sourceMaps()/inline-source-map from production builds. Add npm audit/Snyk to CI. Delete the orphan package-del-lock.json.Confirmed via package-lock.json/package.json versions and webpack.mix.js sourceMaps.
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.
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.
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.Confirmed: no pipeline files in tree; .ftpquota and package-del-lock.json present; only ExampleTest stubs.
d07b7e1be998da23d756303beeb0bf7bbdb96e88) — AP-01.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.!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.'otp' => $otp field from the login response and delete the subin@alist.ae / 1122 backdoor — AP-05, AP-21.creator/otpshow/linkshow inside the auth group and the ~39 unauthenticated admin routes inside ['auth','permission'] — AP-03, AP-06..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.if ($request->ajax()) return $next($request) permission bypass; evaluate ->can() for all requests — AP-02.random_int(), and apply throttle:5,1 to all OTP routes — AP-04, AP-38.avail_days — AP-09 to AP-14.auth:api across routes/api.php, allowlist the dynamic review-column write, and scope notification delete to the owner — AP-15, AP-17.authenticateToken:creator with ownership checks — AP-16.file_get_contents — AP-19./v1/logout revocation route — AP-22, AP-23.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./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.X-Frame-Options: ALLOW-FROM; set Secure/SameSite on the alistToken and session cookies — AP-44, AP-45.$fillable and replace create($request->all()) with validated only() — AP-41, AP-46.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.app/, not app/Models/. Composer autoloads 9 global procedural helpers via the files autoload key. The Repository pattern is partially adopted — most controllers also query Eloquent directly, leading to duplication.Admin/AdminController.php 5 876 LOC, SignupController.php 1 857 LOC, StripeController and SubscriptionController contain webhook logic, validation, persistence, and view return all together. Multiple _bk/_bkp sibling files left behind.app/Repositories/OfferRepository.php at 9 770 LOC is the single biggest risk. Recent commits cluster around it (license restrictions, dedicated offer schedule). Read at least 200 lines of surrounding context before any edit..styleci.yml declares laravel preset — but unused-import removal is disabled, and naming is inconsistent (camelCase, snake_case, and lowercase-leading mix). Inline comments are dense and frequently mark dev-by-dev edits ("Added by SEEMA on 4-Feb-2020", "Added by SAMJIDH on 2-July-2020").predis/predis 1.1.10 (major version behind), fzaninotto/faker 1.4 in dev (abandoned), laravel/ui 4.x still in use, vue 2.5 on the frontend (EOL).*_bkp.php files, commented-out routes throughout, an empty alist-website/ directory, package-del-lock.json (an old package-lock kept around), and a duplicate-key declaration in config/cors.php.use DB; and use \DB facades; App::environment() branching at boot time; raw $_GET access in routes/web.php Google contacts callback.artisan migrate:fresh on a clean DB is slow.# 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
APP_KEY, APP_ENV, APP_DEBUG, APP_URL, APP_CREATOR_URL, APP_ADMIN_URL, APP_BUSINESS_URL, APP_DOMAIN_ADMIN, APP_DOMAIN_BUSINESS, APP_DOMAIN_API, APP_DOMAIN_CREATORDB_CONNECTION, DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORDREDIS_HOST, REDIS_PORT, REDIS_PASSWORD — production points at AWS ElastiCacheQUEUE_CONNECTION=redis, CACHE_DRIVER=redis, BROADCAST_DRIVER=pusherMAIL_* (mail-tracker logs to DB)PUSHER_APP_ID, PUSHER_APP_KEY, PUSHER_APP_SECRET, PUSHER_APP_CLUSTERSTRIPE_KEY, STRIPE_SECRET, STRIPE_WEBHOOK_SECRETAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION, AWS_BUCKETFCM_SERVER_KEY, INTERAKT_API_KEY, MODASH_API_KEY, SENTRY_LARAVEL_DSNADMIN_RATE_LIMIT_PER_MINUTE, ADMIN_STRIKE_LIMIT, ADMIN_STRIKE_WINDOW_MINUTES, ADMIN_BLOCK_DURATION_MINUTESconfig/thirdparty.php is gitignored ("contains webhooks for slack") — but is still git-tracked with live secrets (see AP-07). Fetch the rotated values from ops; do not trust the committed file.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.
storage/fcm.json), Google OAuth clients in .config.json/storage/googlePeople.json/config/thirdparty.php, Slack webhooks + Monday tokens (thirdparty.php), the DB password fallback in config/database.php, Stripe keys in the controllers, the alist-dev-aswin.pem in history, and token.txt. They are visible to everyone who has ever cloned this repo. See findings AP-01, AP-07, AP-08, AP-26, AP-27, AP-28.['auth','permission'] (admin) or authenticateToken:creator (creator) is silently public. ~39 admin routes and a creator block are currently exposed (AP-03, AP-16). Always add new routes inside the inner group.PermissionMiddleware admits any AJAX request without checking permissions (AP-02) — do not rely on it as an authorization boundary until fixed.app/, not app/Models/. Class autoloads are App\Signup etc.files autoload pulls 9 procedural helpers into every request boot. Touching app/Helper/helpers.php et al. affects everything.OfferRepository.php is 9 770 LOC and holds the redemption logic with transactions commented out and raw whereRaw interpolation (AP-14, AP-25, AP-32). Read 200+ lines of context before changing any method; the same logic is copy-pasted across conditional branches.DB::select/whereRaw; several confirmed SQLi already exist in admin controllers (AP-09 to AP-13). Use bindings or the Query Builder.Console/Kernel.php). If general_settings rows are missing, every cron task crashes silently.paymentSucessful, paymentFaild, Insta_mailer, Brodcast, whatappStatus. Do not "fix" them without coordinating — they are wire-format./send-whatsapp-template, /sc, /sc/* (AP-33). CORS is emitted as a wildcard on every response by the global custom middleware (AP-18); Laravel's HandleCors is not wired, so config/cors.php is dead code.Asia/Dubai. All Carbon::now() calls return Dubai time, not UTC..htaccess while composer requires 8.2 (AP-20). Confirm with ops before relying on PHP 8 syntax — the deployment is in an inconsistent state.AuthenticateSession middleware is commented out — password change does not invalidate live sessions.mt_rand, stored plaintext, compared with loose ==, and (for login) echoed in the API response (AP-04, AP-05, AP-22, AP-38). There is also a hardcoded 1122 backdoor for subin@alist.ae (AP-21).*_bk, *_bkp.php) and a 1.7 MB countries-states.json live in the tree. The empty alist-website/ dir is unused.server.js) with wildcard CORS (AP-37). Confirm which one is authoritative before adding broadcast events.image1..image6) are fetched with file_get_contents on attacker-controlled URLs — an SSRF path into the AWS network (AP-19). Use multipart uploads, never remote-fetch raw request strings.PermissionMiddleware) wraps Spatie with hardcoded route exceptions. Check it when an admin user gets unexpected 403s.