Scope & Disclosure
This was an authorized security assessment of an events-management SaaS built on an older CodeIgniter 3 stack. Everything below was reported through responsible disclosure and has been remediated. Every credential, key, hostname, account ID and piece of personal data has been stripped out — this is a high-level walkthrough of the chain, not a copy-paste recipe.
What makes it my favourite find isn't any single exotic exploit. It's the opposite: a medium-severity "exposed file" and a forgotten test script combined into a full cloud compromise, purely because I kept asking one question — "and what can I reach with this?" — at every step. That's how real breaches actually happen.
1. Recon: the app told me exactly what it was running
The API served its Composer dependency manifests (composer.json / composer.lock) with no authentication. A lockfile is a gift to an attacker: it pins every dependency to an exact version, so mapping the install to a list of known CVEs is a lookup, not a guess.
Several pinned packages carried published, high-severity advisories — an HTTP client with credential-leak-on-redirect issues, a PSR-7 implementation with header-injection issues, and a cloud SDK with a path-traversal issue. But the real prize came from a different angle:
- The wildcard TLS certificate leaked a set of internal subdomains (staging, testing, CDN, and a per-tenant instance).
- One tenant subdomain ran a slightly different dependency set than the main API — and that difference included the package that became my way in.
Lesson zero: your certificate transparency logs and your lockfiles are public reconnaissance whether you like it or not.
2. The way in: an unauthenticated LFI (CVE-2023-29887)
A spreadsheet-parsing library shipped a leftover test.php harness that accepted a file path in a GET parameter and read arbitrary files off disk, echoing them back line by line as "spreadsheet rows." No authentication required. CVSS 9.8.
GET /vendor/.../test.php?File=/etc/passwd
Arbitrary file read on a PHP application is rarely just file read. Because the framework's directory layout is well known, I could read the application's own source code and configuration files by absolute path. Reading /etc/passwd and the PHP/webserver config first told me the environment: cloud-hosted Linux, a dedicated low-privilege web user, and — crucially — no disable_functions hardening and config secrets sitting inside the webroot.
3. Turning file-read into the keys to the kingdom
The application kept its secrets in plain PHP config files inside the webroot — exactly the files an LFI primitive can reach. Reading them, in order, gave up:
- Database credentials for development and production (redacted).
- The application signing secret — used both to hash passwords and to sign public "external" URLs.
- Cloud access keys — which, on inspection, turned out to belong to the root account rather than a scoped role.
This is the real theme of the whole engagement: secret sprawl. A single file-read primitive was enough to own everything because every secret lived in a file that primitive could read.
4. Forging "trusted" URLs — IDOR via the app secret
The app protected its public ticket and briefing pages with a hash appended to the URL, roughly:
hash = sha256( reversed_url_segments + app_secret )
That's a reasonable pattern right up until the secret leaks. With the secret recovered in step 3, I could compute a valid hash for any record ID and mint a working link to any user's documents. Classic Insecure Direct Object Reference — except the "unguessable" token was fully forgeable. I verified the algorithm against one known-good URL (the computed hash matched byte-for-byte) to prove the finding, and stopped there rather than harvesting data.
5. Blast radius: from web bug to cloud root
The leaked keys belonged to the cloud root account, which meant control-plane reach over object storage, compute instances and managed databases. The entire engagement was strictly read-only: I confirmed the identity of the credentials and enumerated the reachable resources to document impact, but I wrote nothing, modified no data or configuration, and touched no customer records. The goal of an assessment is to demonstrate impact, not to cause it — proving access is sufficient; abusing it is not.
Put together, the ladder looked like this:
exposed composer.lock -> pick the target dependency
leftover test.php (LFI) -> arbitrary file read
-> db credentials
-> app signing secret -> forge trusted URLs (IDOR)
-> ROOT cloud credentials -> storage + compute + databases
Each rung on its own is a modest bug. Chained, they're critical — because the credentials behind them had no blast-radius limits.
What should have stopped this
- Kill the entry point: delete leftover test/dev scripts, and block
vendor/,application/and dotfiles from the web at the server level. - Stop hardcoding secrets in web-readable config — use environment variables or a secrets manager, and keep them out of the document root.
- Never hand an application root cloud keys. Use least-privilege instance roles so a leak is a contained incident, not a total one.
- Rotate everything after any exposure — keys, DB passwords, and especially the app signing secret, since it underpins URL trust.
- Keep lockfiles out of the webroot and patch known-vulnerable dependencies.
Why I like this one
There's no zero-day here and nothing clever. Just a boring file-read bug, patiently walked up a chain of "and then what?" until it reached the top of the cloud account. Reported, patched, nobody got hurt — and a very clean illustration of why defense in depth and least privilege are not optional.