| @@ -31,8 +31,33 @@ | ||
| 31 | 31 | $this->db = $wpdb; |
| 32 | 32 | } |
| 33 | 33 | |
| 34 | 34 | /** |
| 35 | + * Validate a SQL identifier (column name) against a strict allow-list. | |
| 36 | + * | |
| 37 | + * Only plain identifiers ([A-Za-z0-9_]) are permitted. Since column names | |
| 38 | + * cannot be parameterized by wpdb::prepare(), any value used as a column | |
| 39 | + * name must be validated here to prevent SQL injection when the array keys | |
| 40 | + * originate from user-controlled request bodies. | |
| 41 | + * | |
| 42 | + * @param string $identifier | |
| 43 | + * | |
| 44 | + * @return string The validated identifier, wrapped in backticks. | |
| 45 | + * | |
| 46 | + * @throws RuntimeException When the identifier is not a plain column name. | |
| 47 | + */ | |
| 48 | + private function sanitize_identifier( string $identifier ): string { | |
| 49 | + if ( ! preg_match( '/^[a-zA-Z0-9_]+$/', $identifier ) ) { | |
| 50 | + throw new RuntimeException( | |
| 51 | + /* translators: %s: column name. */ | |
| 52 | + sprintf( esc_html__( 'Invalid database column identifier: %s', 'booktics' ), esc_html( $identifier ) ) | |
| 53 | + ); | |
| 54 | + } | |
| 55 | + | |
| 56 | + return "`{$identifier}`"; | |
| 57 | + } | |
| 58 | + | |
| 59 | + /** | |
| 35 | 60 | * Begin database transaction |
| 36 | 61 | * |
| 37 | 62 | * @return void |
| 38 | 63 | */ |
| @@ -71,9 +96,9 @@ | ||
| 71 | 96 | $conditions = array(); |
| 72 | 97 | $values = array(); |
| 73 | 98 | |
| 74 | 99 | foreach ( $where as $key => $value ) { |
| 75 | - $conditions[] = "$key = %s"; | |
| 100 | + $conditions[] = $this->sanitize_identifier( $key ) . ' = %s'; | |
| 76 | 101 | $values[] = $value; |
| 77 | 102 | } |
| 78 | 103 | |
| 79 | 104 | $condition_string = implode( ' AND ', $conditions ); |
| @@ -106,8 +131,9 @@ | ||
| 106 | 131 | } |
| 107 | 132 | |
| 108 | 133 | $table = booktics_get_table_name( $table ); |
| 109 | 134 | $columns = array_keys( $records[0] ); |
| 135 | + $safe_columns = array_map( array( $this, 'sanitize_identifier' ), $columns ); | |
| 110 | 136 | $placeholders = array_fill( 0, count( $columns ), '%s' ); |
| 111 | 137 | $placeholder_string = '(' . implode( ',', $placeholders ) . ')'; |
| 112 | 138 | |
| 113 | 139 | $values = array(); |
| @@ -119,9 +145,9 @@ | ||
| 119 | 145 | $values[] = $record[ $column ] ?? ''; |
| 120 | 146 | } |
| 121 | 147 | } |
| 122 | 148 | |
| 123 | - $column_string = implode( ',', $columns ); | |
| 149 | + $column_string = implode( ',', $safe_columns ); | |
| 124 | 150 | $placeholders_string = implode( ',', $placeholders_groups ); |
| 125 | 151 | |
| 126 | 152 | $sql = "INSERT INTO $table ($column_string) VALUES $placeholders_string"; |
| 127 | 153 | |
| @@ -145,9 +171,17 @@ | ||
| 145 | 171 | * @return array |
| 146 | 172 | */ |
| 147 | 173 | public function select( string $table, array $where = array(), array $columns = array( '*' ) ) { |
| 148 | 174 | $table = booktics_get_table_name( $table ); |
| 149 | - $columns_s = implode( ', ', $columns ); | |
| 175 | + $columns_s = implode( | |
| 176 | + ', ', | |
| 177 | + array_map( | |
| 178 | + function ( $column ) { | |
| 179 | + return '*' === $column ? '*' : $this->sanitize_identifier( $column ); | |
| 180 | + }, | |
| 181 | + $columns | |
| 182 | + ) | |
| 183 | + ); | |
| 150 | 184 | $sql = "SELECT $columns_s FROM $table"; |
| 151 | 185 | |
| 152 | 186 | $values = array(); |
| 153 | 187 | if ( ! empty( $where ) ) { |
| @@ -153,9 +187,9 @@ | ||
| 153 | 187 | if ( ! empty( $where ) ) { |
| 154 | 188 | $conditions = array(); |
| 155 | 189 | |
| 156 | 190 | foreach ( $where as $key => $value ) { |
| 157 | - $conditions[] = "$key = %s"; | |
| 191 | + $conditions[] = $this->sanitize_identifier( $key ) . ' = %s'; | |
| 158 | 192 | $values[] = $value; |
| 159 | 193 | } |
| 160 | 194 | |
| 161 | 195 | $condition_string = implode( ' AND ', $conditions ); |
| @@ -201,14 +235,14 @@ | ||
| 201 | 235 | $where_conditions = array(); |
| 202 | 236 | $values = array(); |
| 203 | 237 | |
| 204 | 238 | foreach ( $data as $key => $value ) { |
| 205 | - $set_values[] = "$key = %s"; | |
| 239 | + $set_values[] = $this->sanitize_identifier( $key ) . ' = %s'; | |
| 206 | 240 | $values[] = $value; |
| 207 | 241 | } |
| 208 | 242 | |
| 209 | 243 | foreach ( $where as $key => $value ) { |
| 210 | - $where_conditions[] = "$key = %s"; | |
| 244 | + $where_conditions[] = $this->sanitize_identifier( $key ) . ' = %s'; | |
| 211 | 245 | $values[] = $value; |
| 212 | 246 | } |
| 213 | 247 | |
| 214 | 248 | $set_string = implode( ', ', $set_values ); |