Leaked Token to Super-Admin Takeover and Production Database Injection
The Risk
An attacker could take full control of the system that runs the company's website content and reach all the way into the company's main database, with no login and no password. A secret key was left sitting inside the public website's own code, where any visitor could copy it. From there the attacker could trick the system into revealing a staff member's email, reset that staff member's password without ever seeing their inbox, and become the highest-level administrator. With that access they could plant their own commands inside the live database and rewrite what real customers see on the site, including pages that ask people to connect their crypto wallets.
The Vulnerability
The target ran a headless content management system to power its public marketing site. Three independent weaknesses chained together into a full takeover:
- A 256-character access token for the content system's API was inlined directly into the public website's JavaScript bundles. Every visitor received it, and the files were served by the CDN with
cache-control: public, max-age=31536000, immutable, so each edge node held the token for a year. - The content system's query layer accepted a legacy filter syntax that bypassed the schema sanitizer. This let a read-only token reach private columns on the internal admin-users table, turning a content endpoint into a one-bit data oracle.
- The content modeling layer accepted a column default wrapped in a raw flag, passing the value verbatim into the next
CREATE TABLEstatement, a database-level (DDL) injection sink.
None of the three closes the finding on its own. Rotating the token leaves the post-authentication injection sink reachable, and upgrading the content system leaves the leaked token granting read access to every content type.
The Attack
Token Discovery
The public marketing site referenced a set of static JavaScript chunks. Scraping the homepage for those chunk URLs, fetching each one, and grepping for a Bearer literal pulled the token out in a single pass. No file hash was hardcoded, so the technique survived every site rebuild even as the chunk filenames rotated.
With the token attached as an Authorization: Bearer header, the content API returned data on every content type. Without it, the same request returned 403, confirming the token was the only access precondition.
Admin Email Enumeration via Relation Oracle
The schema sanitizer rejected modern filter keys against the admin relation, but a legacy nested-filter syntax skipped sanitization and reached the query builder unchanged. A request like filter[modifiedBy][email][startsWith]=k emitted a join against the internal admin-users table, and the pagination total reflected whether the prefix matched.
Walking an alphabet one character at a time, the live admin email was reconstructed end-to-end from zero knowledge:
filter[modifiedBy][email][startsWith]=k -> total > 0
filter[modifiedBy][email][startsWith]=ke -> total > 0
filter[modifiedBy][email][startsWith]=kev -> total > 0
...continue until the full address resolves The underscore character was deliberately omitted from the alphabet because the underlying LIKE comparison treats it as a single-character wildcard, which would have produced false matches.
Unauthenticated Password Reset
A POST /api/internal/forgot-password for the enumerated address returned 204 and silently rotated that admin's reset-password token to a fresh 40-character hex value in the database. That private column was then read back through the very same relation oracle, one hex character at a time, using filter[modifiedBy][resetToken][startsWith]=....
On production this extraction was capped at the first six hex characters to avoid hijacking the live account. The full chain (40-character extraction, POST /api/internal/reset-password, super-admin session) was run end-to-end against a local instance of the same major version to demonstrate the outcome without harming production.
Database-Level Injection
Holding a super-admin session, a single POST /api/internal/content-types submitted a content type whose column default was marked raw:
{
"attributes": {
"data": {
"type": "string",
"column": { "default": "<sql>", "raw": true }
}
}
} The raw flag caused the value to be passed verbatim into the next CREATE TABLE, inlining attacker SQL into the table's DEFAULT clause. The database evaluates that expression at insert time, allowing constructs such as current_user, current_database(), file reads, or a sleep call for insert-time denial of service. Because the malicious schema is committed to disk, it survives admin password rotation and admin-user removal. The production sink was confirmed reachable by status code without ever posting a real content type there.
The Impact
The chain produced two independent critical-class outcomes, either sufficient on its own:
- Full content takeover with cross-site pivot. Super-admin control over the content system meant any blog or support article, the site-wide notification banner, and the primary call-to-action could be rewritten. Stored script in the security origin of the marketing site gained messaging access to the in-scope application origin, enabling crypto wallet phishing where the address bar still read the legitimate domain until the click. Seed-phrase or wallet-connection compromise is instant and unrecoverable.
- Arbitrary SQL on the production database. The raw-default injection placed attacker-controlled SQL into the production PostgreSQL backend, evaluated at insert time and persistent on disk across credential rotation.
The entry point was a credential shipped in the company's own front-end code, recoverable from every CDN edge, browser caches, and web archives for as long as the bundle was cached. The takeover required no login at any step until the final administrative actions, which used credentials the attacker set themselves.
Remediation
All three fixes are required; none individually closes the finding.
- Rotate the leaked token immediately. Delete the exposed API token, issue a replacement, and rebuild the front end so the new token is read from a server-side environment variable instead of being inlined into a browser-shipped bundle. Treat the original token as already compromised.
- Force a password reset for every admin user. Enumerate the full admin set via the oracle, reset all of them, and audit login timestamps and token/role audit-log entries between the leaked bundle's publish date and the rotation timestamp.
- Upgrade the content system to a version that closes both the relation-filter oracle and the raw-default injection sink at the vendor level.
- Interim: drop any request carrying the leaked token at the reverse proxy, reject query strings that filter on internal author-relation fields such as
modifiedBy,createdBy, orpublishedBy, and rate-limit the forgot-password endpoint. - Process: grep the front-end build output for
Bearerliterals before every deploy.