[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fNEd2PBX97O9mw8QRCoTNzIOqXcX19SMMlotn_dzoQ1w":3},{"id":4,"url_slug":5,"title":6,"description":7,"plugin_slug":8,"theme_slug":9,"affected_versions":10,"patched_in_version":11,"severity":12,"cvss_score":13,"cvss_vector":14,"vuln_type":15,"published_date":16,"updated_date":17,"references":18,"days_to_patch":20,"patch_diff_files":21,"patch_trac_url":9,"research_status":28,"research_verified":29,"research_rounds_completed":30,"research_plan":31,"research_summary":32,"research_vulnerable_code":33,"research_fix_diff":34,"research_exploit_outline":35,"research_model_used":36,"research_started_at":37,"research_completed_at":38,"research_error":9,"poc_status":9,"poc_video_id":9,"poc_summary":9,"poc_steps":9,"poc_tested_at":9,"poc_wp_version":9,"poc_php_version":9,"poc_playwright_script":9,"poc_exploit_code":9,"poc_has_trace":29,"poc_model_used":9,"poc_verification_depth":9,"poc_exploit_code_gated":29,"source_links":39},"CVE-2026-40762","wpgraphql-unauthenticated-sql-injection","WPGraphQL \u003C 2.11.1 - Unauthenticated SQL Injection","The WPGraphQL plugin for WordPress is vulnerable to SQL Injection in versions up to 2.11.1 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for unauthenticated attackers to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.","wp-graphql",null,"\u003C2.11.1","2.11.1","high",7.5,"CVSS:3.1\u002FAV:N\u002FAC:L\u002FPR:N\u002FUI:N\u002FS:U\u002FC:H\u002FI:N\u002FA:N","Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')","2026-04-21 00:00:00","2026-04-30 14:43:47",[19],"https:\u002F\u002Fwww.wordfence.com\u002Fthreat-intel\u002Fvulnerabilities\u002Fid\u002F1410ee86-90ec-4913-bc74-d8954d141d72?source=api-prod",10,[22,23,24,25,26,27],"constants.php","languages\u002Fwp-graphql.pot","readme.txt","src\u002FData\u002FLoader\u002FUserLoader.php","vendor\u002Fcomposer\u002Finstalled.php","wp-graphql.php","researched",false,3,"# Exploitation Research Plan - CVE-2026-40762\n\n## 1. Vulnerability Summary\nThe **WPGraphQL** plugin (versions \u003C 2.11.1) contains an unauthenticated SQL injection vulnerability in its `UserLoader` class. The flaw exists because the plugin uses numbered placeholders (e.g., `%1$s`) within a `$wpdb->prepare()` call. \n\nWordPress's `$wpdb->prepare()` implementation specifically looks for literal `%s` (string), `%d` (integer), or `%f` (float) placeholders to perform escaping and quoting. It does **not** recognize numbered placeholders like `%1$s`. Consequently, when the query is finally processed by `vsprintf`, the user-supplied input is interpolated directly into the SQL statement without being escaped or enclosed in quotes.\n\n## 2. Attack Vector Analysis\n- **Endpoint:** The standard GraphQL endpoint (default: `\u002Fgraphql` or `\u002F?graphql=1`).\n- **Authentication:** Unauthenticated (by default).\n- **GraphQL Field:** The `nodes` query or the `users` query's `include` argument.\n- **Vulnerable Parameter:** The `ids` (within the `nodes` query) or `include` (within `users(where: ...)`).\n- **Payload Transport:** The payload is carried inside a Base64-encoded string representing a GraphQL `ID`.\n\n## 3. Code Flow\n1. **Entry Point:** An unauthenticated user sends a GraphQL request to the endpoint.\n2. **Parsing:** The `WPGraphQL\\Router` processes the query. If the query is `nodes(ids: [...])`, it iterates through the provided IDs.\n3. **ID Decoding:** WPGraphQL IDs are typically Base64 encoded strings in the format `typename:id` (e.g., `user:1`).\n4. **Loader Execution:** For IDs with the type `user`, the `WPGraphQL\\Data\\Loader\\UserLoader` is invoked via the `loadKeys()` method (line 120 of `src\u002FData\u002FLoader\u002FUserLoader.php`).\n5. **Vulnerable Sink:** `loadKeys()` calls `get_public_users( array $keys )` (line 144).\n6. **Query Construction:**\n   - `$keys` contains the raw IDs extracted from the Base64 input.\n   - `$ids = implode( ', ', $keys )` (line 90).\n   - The query is built using `$wpdb->prepare()` on line 96:\n     ```php\n     $wpdb->prepare(\n         \"SELECT DISTINCT $wpdb->users.ID FROM $wpdb->posts INNER JOIN $wpdb->users ON post_author = $wpdb->users.ID $where AND post_author IN ( %1\\$s ) ORDER BY FIELD( $wpdb->users.ID, %2\\$s)\",\n         $ids,\n         $ids\n     )\n     ```\n7. **Injection:** Because `%1$s` is used, `$wpdb->prepare` fails to escape `$ids`. `vsprintf` then performs raw interpolation.\n\n## 4. Nonce Acquisition Strategy\nBy default, the WPGraphQL endpoint does **not** require a nonce for unauthenticated queries. If the setting \"Restrict Endpoint to Authenticated Users\" is enabled, the vulnerability is still present but requires a valid user session.\n\nIf the environment has been hardened to require a nonce for the GraphQL endpoint (uncommon but possible), it usually relies on the standard WordPress REST API nonce (`wp_rest`).\n\n**Strategy:**\n1. Check if the endpoint responds to a simple query without a nonce.\n2. If a nonce is required, it is typically exposed via `wp_localize_script` under the key `window.wpgraphql_settings?.nonce` or similar.\n3. **Creation:** Create a page with the `[wpgraphql]` shortcode (if available) or navigate to the admin dashboard (if authenticated).\n4. **Extraction:** `browser_eval(\"window.wpgraphql_settings?.nonce\")`.\n\n*Note: In most default installations, no nonce is required for the payload below.*\n\n## 5. Exploitation Strategy\nWe will use a time-based blind SQL injection via the `nodes` query.\n\n### Step 1: Baseline Request\nConfirm the endpoint is active and identify the Base64 ID format.\n```json\n{\n  \"query\": \"query { users(first: 1) { nodes { id } } }\"\n}\n```\nIf the ID returned is `dXNlcjox` (`user:","WPGraphQL is vulnerable to unauthenticated SQL injection in the UserLoader class because it uses numbered placeholders (e.g., %1$s) within $wpdb->prepare(). WordPress's prepare implementation does not recognize these placeholders for escaping, resulting in raw interpolation of user-supplied input into SQL queries via vsprintf.","\u002F* src\u002FData\u002FLoader\u002FUserLoader.php line 89 *\u002F\n$where = get_posts_by_author_sql( $post_types, true, $author_id, $public_only );\n$ids   = implode( ', ', $keys );\n\nglobal $wpdb;\n\n$results = $wpdb->get_results( \u002F\u002F phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching\n    $wpdb->prepare(\n        \"SELECT DISTINCT $wpdb->users.ID FROM $wpdb->posts INNER JOIN $wpdb->users ON post_author = $wpdb->users.ID $where AND post_author IN ( %1\\$s ) ORDER BY FIELD( $wpdb->users.ID, %2\\$s)\", \u002F\u002F phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnquotedComplexPlaceholder,WordPressVIPMinimum.Variables.RestrictedVariables.user_meta__wpdb__users\n        $ids,\n        $ids\n    )\n);\n\n---\n\n\u002F* src\u002FData\u002FLoader\u002FUserLoader.php line 144 *\u002F\npublic function loadKeys( array $keys ) {\n    if ( empty( $keys ) ) {\n        return $keys;\n    }\n    \u002F\u002F ... (truncated)\n    \u002F**\n     * Determine which of the users are public (have published posts).\n     *\u002F\n    $public_users = $this->get_public_users( $keys );","--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fwp-graphql\u002F2.11.0\u002Fsrc\u002FData\u002FLoader\u002FUserLoader.php\t2024-08-21 17:58:04.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fwp-graphql\u002F2.11.1\u002Fsrc\u002FData\u002FLoader\u002FUserLoader.php\t2026-04-10 18:20:34.000000000 +0000\n@@ -27,6 +27,33 @@\n \t}\n \n \t\u002F**\n+\t * Normalize a loader key to a WordPress user database ID.\n+\t *\n+\t * Only non-empty digit-only strings and positive integers are accepted so values\n+\t * such as \"0) OR …\" cannot pass `absint`-based checks elsewhere and reach SQL.\n+\t *\n+\t * @param mixed $key Loader key (typically an integer or numeric string).\n+\t *\n+\t * @return int|null Positive user ID, or null if the key is not a valid ID.\n+\t *\u002F\n+\tprivate function parse_user_database_id( $key ): ?int {\n+\t\tif ( is_int( $key ) ) {\n+\t\t\treturn $key > 0 ? $key : null;\n+\t\t}\n+\n+\t\tif ( is_string( $key ) ) {\n+\t\t\tif ( '' === $key || ! ctype_digit( $key ) ) {\n+\t\t\t\treturn null;\n+\t\t\t}\n+\t\t\t$id = absint( $key );\n+\n+\t\t\treturn $id > 0 ? $id : null;\n+\t\t}\n+\n+\t\treturn null;\n+\t}\n+\n+\t\u002F**\n \t * The data loader always returns a user object if it exists, but we need to\n \t * separately determine whether the user should be considered private. The\n \t * WordPress frontend does not expose authors without published posts, so our\n@@ -46,6 +73,20 @@\n \t * @return array\u003Cint,bool> Associative array of author IDs (int) to boolean.\n \t *\u002F\n \tpublic function get_public_users( array $keys ) {\n+\t\t$sanitized_keys = [];\n+\t\tforeach ( $keys as $key ) {\n+\t\t\t$id = $this->parse_user_database_id( $key );\n+\t\t\tif ( null !== $id ) {\n+\t\t\t\t$sanitized_keys[] = $id;\n+\t\t\t}\n+\t\t}\n+\t\t$sanitized_keys = array_values( array_unique( $sanitized_keys ) );\n+\n+\t\tif ( empty( $sanitized_keys ) ) {\n+\t\t\treturn [];\n+\t\t}\n+\n+\t\t$keys = $sanitized_keys;","The exploit targets the standard GraphQL endpoint (usually \u002Fgraphql). An unauthenticated attacker sends a 'nodes' or 'users' query containing a malicious Base64-encoded ID. The ID must be prefixed with 'user:' followed by the SQL payload, for example: 'user:1) OR SLEEP(5)--'. When the UserLoader decodes this ID, it extracts the payload and passes it to get_public_users. Inside this method, the payload is imploded into a string and processed by $wpdb->prepare() using a numbered placeholder (%1$s). Because $wpdb->prepare() ignores numbered placeholders, the payload is concatenated directly into the SQL query's WHERE and ORDER BY clauses without escaping, enabling time-based or boolean-based blind SQL injection.","gemini-3-flash-preview","2026-05-04 19:03:31","2026-05-04 19:04:27",{"type":40,"vulnerable_version":41,"fixed_version":11,"vulnerable_browse":42,"vulnerable_zip":43,"fixed_browse":44,"fixed_zip":45,"all_tags":46},"plugin","2.11.0","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fwp-graphql\u002Ftags\u002F2.11.0","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Fwp-graphql.2.11.0.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fwp-graphql\u002Ftags\u002F2.11.1","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Fwp-graphql.2.11.1.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fwp-graphql\u002Ftags"]