| @@ -263,11 +263,11 @@ | ||
| 263 | 263 | //If not column then use wpdb prepare |
| 264 | 264 | //if contains $prefix |
| 265 | 265 | $contain_join = preg_replace( '/^(\s?AND ?|\s?OR ?)|\s$/i', '', $param2 ); |
| 266 | 266 | |
| 267 | - $param2 = is_array( $param2 ) ? ( '("' . implode( '","', $param2 ) . '")' ) : ( $param2 === null | |
| 267 | + $param2 = is_array( $param2 ) ? ( '(' . implode( ',', array_map( [ $this, 'bind_value' ], $param2 ) ) . ')' ) : ( $param2 === null | |
| 268 | 268 | ? 'null' |
| 269 | - : ( strpos( $param2, '.' ) !== false || strpos( $param2, $wpdb->prefix ) !== false ? $param2 : $wpdb->prepare( is_numeric( $param2 ) ? '%d' : '%s', $param2 ) ) | |
| 269 | + : ( $this->is_column_reference( $param2 ) ? $param2 : $this->bind_value( $param2 ) ) | |
| 270 | 270 | ); |
| 271 | 271 | |
| 272 | 272 | $this->where[] = [ |
| 273 | 273 | 'joint' => $joint, |
| @@ -508,12 +508,12 @@ | ||
| 508 | 508 | $referenceKey = $operator; |
| 509 | 509 | $operator = '='; |
| 510 | 510 | } |
| 511 | 511 | |
| 512 | - $referenceKey = is_array( $referenceKey ) ? ( '(\'' . implode( '\',\'', $referenceKey ) . '\')' ) | |
| 512 | + $referenceKey = is_array( $referenceKey ) ? ( '(' . implode( ',', array_map( [ $this, 'bind_value' ], $referenceKey ) ) . ')' ) | |
| 513 | 513 | : ( $referenceKey === null |
| 514 | 514 | ? 'null' |
| 515 | - : ( strpos( $referenceKey, '.' ) !== false || strpos( $referenceKey, $wpdb->prefix ) !== false ? $referenceKey : $wpdb->prepare( is_numeric( $referenceKey ) ? '%d' : '%s', $referenceKey ) ) | |
| 515 | + : ( $this->is_column_reference( $referenceKey ) ? $referenceKey : $this->bind_value( $referenceKey ) ) | |
| 516 | 516 | ); |
| 517 | 517 | |
| 518 | 518 | $join['on'][] = [ |
| 519 | 519 | 'joint' => $joint, |
| @@ -1354,8 +1354,66 @@ | ||
| 1354 | 1354 | $query .= trim( 'DELETE ' . ( count( $this->join ) |
| 1355 | 1355 | ? preg_replace( '/\s[aA][sS][\s\S]+.*?/', '', $this->from ) |
| 1356 | 1356 | : '' |
| 1357 | 1357 | ) ); |
| 1358 | + } | |
| 1359 | + | |
| 1360 | + /** | |
| 1361 | + * Decide whether a right-hand operand is a column reference rather than a value. | |
| 1362 | + * | |
| 1363 | + * `where()` and `join()` accept either on the right: `join('nx_stats b', 'b.nx_id', | |
| 1364 | + * '=', 'a.nx_id')` compares two columns, while `where('title', 'LIKE', $search)` | |
| 1365 | + * compares a column to a value. Only a value may be bound, so the two have to be | |
| 1366 | + * told apart -- and the test used to be "does it contain a dot, or the table | |
| 1367 | + * prefix?". That is true of `a.nx_id`, but it is also true of any search term | |
| 1368 | + * carrying a dot, so `?s=.%27%20UNION%20SELECT...` was emitted into the statement | |
| 1369 | + * verbatim and the LIKE became an injection point. | |
| 1370 | + * | |
| 1371 | + * The test is now the shape of a *qualified* identifier -- `table.column`, each half | |
| 1372 | + * optionally backquoted -- so a real column reference still passes through unbound | |
| 1373 | + * while anything carrying a quote, space, comment marker or wildcard falls through | |
| 1374 | + * to {@see self::bind_value()}. | |
| 1375 | + * | |
| 1376 | + * The qualifier is required on purpose. A bare `published` is far more likely to be | |
| 1377 | + * a value than a column, and it was always bound under the old test too, so treating | |
| 1378 | + * unqualified names as columns here would silently turn `where('status', | |
| 1379 | + * 'published')` into a comparison against a non-existent column. | |
| 1380 | + * | |
| 1381 | + * @param mixed $value Right-hand operand. | |
| 1382 | + * | |
| 1383 | + * @return bool | |
| 1384 | + */ | |
| 1385 | + private function is_column_reference( $value ) { | |
| 1386 | + if ( ! is_string( $value ) || '' === $value ) { | |
| 1387 | + return false; | |
| 1388 | + } | |
| 1389 | + | |
| 1390 | + return (bool) preg_match( '/^`?[A-Za-z_][A-Za-z0-9_]*`?\.`?[A-Za-z_][A-Za-z0-9_]*`?$/', $value ); | |
| 1391 | + } | |
| 1392 | + | |
| 1393 | + /** | |
| 1394 | + * Bind a value into the statement through $wpdb->prepare(). | |
| 1395 | + * | |
| 1396 | + * @param mixed $value Value to bind. | |
| 1397 | + * | |
| 1398 | + * @return string Quoted, escaped SQL literal. | |
| 1399 | + */ | |
| 1400 | + private function bind_value( $value ) { | |
| 1401 | + global $wpdb; | |
| 1402 | + | |
| 1403 | + if ( null === $value ) { | |
| 1404 | + return 'null'; | |
| 1405 | + } | |
| 1406 | + if ( is_bool( $value ) ) { | |
| 1407 | + return $wpdb->prepare( '%d', $value ? 1 : 0 ); | |
| 1408 | + } | |
| 1409 | + if ( ! is_numeric( $value ) ) { | |
| 1410 | + return $wpdb->prepare( '%s', $value ); | |
| 1411 | + } | |
| 1412 | + | |
| 1413 | + // %d on a float silently truncates it, so anything with a decimal point or an | |
| 1414 | + // exponent takes %f. Integer strings -- '007' included -- keep %d as before. | |
| 1415 | + return $wpdb->prepare( ( is_float( $value ) || false !== strpbrk( (string) $value, '.eE' ) ) ? '%f' : '%d', $value ); | |
| 1358 | 1416 | } |
| 1359 | 1417 | |
| 1360 | 1418 | /** |
| 1361 | 1419 | * Sanitize value. |