fix/SQL injection + missing authorization in the string auto-translate AJAX - #41
Open
SNO7E-G wants to merge 1 commit into
Open
fix/SQL injection + missing authorization in the string auto-translate AJAX#41SNO7E-G wants to merge 1 commit into
SNO7E-G wants to merge 1 commit into
Conversation
The generate_strings_translations() AJAX handler interpolated $_POST['offset'] (and $_POST['count']) directly into a LIMIT clause, allowing SQL injection, and was protected only by a nonce so any logged-in user could reach it. Cast offset and count with absint() before use (keeping the `false` sentinel that detects the first batch), and require the manage_options capability that already gates the plugin's menus.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Sanitizes the paging parameters that reach a SQL
LIMITclause and requiresmanage_optionson thewp_ajax_generate_strings_translations_actionendpoint.Problem
generate_strings_translations()built... LIMIT {$offset}, {$limit}with$offsettaken directly from$_POST['offset'].$contextswas alreadyesc_sql()-quoted and$limitis a hardcoded100, but$offsetwas unsanitized — an injection point. Theif ( $offset <= $count )guard does not sanitize: PHP's loose<=compares a non-numeric string without rejecting it. The endpoint was protected only by a nonce, which is a CSRF token, not an authorization check.Fix & changes
inc/wpml-compatibility-test-tools.class.php, one hunk:$offsetand$countare cast withabsint().$countkeeps itsfalsesentinel (isset(...) ? absint(...) : false) — line 197 relies on$count === falseto detect the first batch and run theCOUNT(*).current_user_can( 'manage_options' )→wp_send_json_error( 'forbidden', 403 )after the nonce check.Risk
Low.
absint()is the identity on the non-negative integers the server itself emitted in the previous response (res/js/mt-script.js:265-273echoesoffset/countback), so paging, the$offset += $limitaccumulation and the progress math are unchanged.manage_optionsmatches all five menu registrations (class.php:309-325). Review noted the practical pre-fix exposure was lower than "any logged-in user" — WP nonces are bound to user ID and session, and the nonce field is only rendered on amanage_optionspage — so this is defence in depth plus a real injection fix. The commit message is worded accordingly.Verified tests (
t1-sqli-cap.php)php -lclean.generate_strings_translations()executed, SQL captured from$wpdb. With$_POST['offset'] = "0 UNION SELECT ID,user_login,user_pass,user_email FROM wp_users -- ":… WHERE context IN ('my-theme') LIMIT 0 UNION SELECT ID,user_login,user_pass,user_email FROM wp_users -- , 100… WHERE context IN ('my-theme') LIMIT 0, 100{"offset":100,"count":"250","progress":40}and runs 1 query; branch returns{"success":false,"data":"forbidden"}with HTTP 403 and runs 0 queries. Administrator unaffected; a bad nonce stillwp_die()s through the realcheck_ajax_referer.absint()(confirmed live viaReflectionFunctionto beload.php:1468) on100 UNION SELECT …→100;1'; DROP TABLE wp_users; --→1;0 PROCEDURE ANALYSE(EXTRACTVALUE(1,CONCAT(0x3a,version())),1)→0;abc/''→0;-5→5;300→300.wp_send_json_error( $value, $status_code, $flags )confirmed atfunctions.php:4614; terminates viawp_die().offset) is proven by execution above. The other variable in the same query,context IN ({$esc_contexts}), is pre-existing code that this PR does not modify. It was audited separately and is safe, confirmed by reading core:esc_sql()→wpdb::_escape()→wpdb::_real_escape()(class-wpdb.php:1271), which usesmysqli_real_escape_string()whenever a DB connection exists (line 1276-1277) — i.e. always in production. That is connection-charset aware, which is what defeats the multibyte/GBK-style bypass that plainaddslashes()is vulnerable to.addslashes()is only the fallback for a connectionlesswpdb, which core itself flags via_doing_it_wrong()(line 1283)."'" . esc_sql( $context ) . "'"), so it lands as a quoted string literal — the canonical safe pattern. (Escaping is only insufficient when a value is interpolated unquoted, which was exactly theoffsetcase this PR fixes.)_real_escape()returns''(line 1272-1274), and a nested array (contexts[0][]=x) makes_escape()return an array, so concatenation yields the literal'Array'plus a PHP notice — a query that matches nothing. Worth tidying one day; not a vulnerability, and not a regression from this PR.wpdb::_escapewithaddslashes(proving realmysqli_real_escape_stringbehaviour would need a live MySQL connection), so this conclusion rests onsource review, not execution. Recorded that way deliberately.
countchanges from string"250"to int250; the JS only posts it back.