PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.1
1.7.1 1.7.0 1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 All 51 releases
← All changes | app/Models/BatchQuery/Batch.php +103 -18 1.3.27 → 1.7.1 View file →
@@ -27,11 +27,17 @@
27 27 if (!isset($index) || empty($index)) {
28 28 $index = $table->getKeyName();
29 29 }
30 30
31 + // The keys of each row become raw SQL column identifiers below. They can
32 + // originate from user input (e.g. the products/bulk-update payload), so an
33 + // unvalidated key carrying a backtick breaks out of the identifier context
34 + // and injects SQL. Reject anything that is not a plain column identifier.
35 + $this->assertIdentifier($index);
36 +
31 37 $driver = $table->getConnection()->getName();
32 38 foreach ($values as $key => $val) {
33 - $ids[] = $val[$index];
39 + $ids[] = $this->db->prepare('%s', $val[$index]);
34 40
35 41 if ($table->usesTimestamps()) {
36 42 $updatedAtColumn = $table->getUpdatedAtColumn();
37 43
@@ -41,8 +47,10 @@
41 47 }
42 48
43 49 foreach (array_keys($val) as $field) {
44 50 if ($field !== $index) {
51 + $this->assertIdentifier($field);
52 + $indexValue = $this->db->prepare('%s', $val[$index]);
45 53 // If increment / decrement
46 54 if (gettype($val[$field]) == 'array') {
47 55
48 56 $isMathOperator = true;
@@ -68,21 +76,37 @@
68 76 $value = '`' . $field . '`' . $val[$field][0] . $val[$field][1];
69 77 }
70 78 }
71 79 else{
72 - $value = "'" . json_encode($val[$field]) . "'";
80 + // Array values are serialized to JSON and dropped into the
81 + // SQL as a string literal. json_encode() does NOT escape
82 + // single quotes, so an array value such as other_info
83 + // carrying a quote breaks out of the literal and injects
84 + // SQL. prepare('%s', ...) adds the quotes and escapes the
85 + // payload (quotes and backslashes) safely.
86 + $value = $this->db->prepare('%s', json_encode($val[$field]));
73 87 }
74 88
75 89 } else {
76 - // Only update
77 - $finalField = $raw ? Common::mysqlEscape($val[$field]) : "'" . Common::mysqlEscape($val[$field]) . "'";
78 - $value = (is_null($val[$field]) ? 'NULL' : $finalField);
90 + // Scalar value. prepare('%s', ...) both quotes and fully
91 + // escapes it. Common::mysqlEscape() must NOT be used here:
92 + // it treats a JSON-valid scalar string (e.g. the literal
93 + // "O'Reilly") specially and returns it decoded but unescaped,
94 + // so the apostrophe would break out of the string literal.
95 + // $raw is an internal-only path (no caller passes it).
96 + if (is_null($val[$field])) {
97 + $value = 'NULL';
98 + } elseif ($raw) {
99 + $value = Common::mysqlEscape($val[$field]);
100 + } else {
101 + $value = $this->prepareScalar($table, $val[$field]);
102 + }
79 103 }
80 104
81 105 if (Common::disableBacktick($driver))
82 - $final[$field][] = 'WHEN ' . $index . ' = \'' . $val[$index] . '\' THEN ' . $value . ' ';
106 + $final[$field][] = 'WHEN ' . $index . ' = ' . $indexValue . ' THEN ' . $value . ' ';
83 107 else
84 - $final[$field][] = 'WHEN `' . $index . '` = \'' . $val[$index] . '\' THEN ' . $value . ' ';
108 + $final[$field][] = 'WHEN `' . $index . '` = ' . $indexValue . ' THEN ' . $value . ' ';
85 109 }
86 110 }
87 111 }
88 112
@@ -93,9 +117,9 @@
93 117 $cases .= '"' . $k . '" = (CASE ' . implode("\n", $v) . "\n"
94 118 . 'ELSE "' . $k . '" END), ';
95 119 }
96 120
97 - $query = "UPDATE \"" . $this->getFullTableName($table) . '" SET ' . substr($cases, 0, -2) . " WHERE \"$index\" IN('" . implode("','", $ids) . "');";
121 + $query = "UPDATE \"" . $this->getFullTableName($table) . '" SET ' . substr($cases, 0, -2) . " WHERE \"$index\" IN(" . implode(",", $ids) . ");";
98 122
99 123 } else {
100 124
101 125 $cases = '';
@@ -103,9 +127,9 @@
103 127 $cases .= '`' . $k . '` = (CASE ' . implode("\n", $v) . "\n"
104 128 . 'ELSE `' . $k . '` END), ';
105 129 }
106 130
107 - $query = "UPDATE `" . $this->getFullTableName($table) . "` SET " . substr($cases, 0, -2) . " WHERE `$index` IN(" . '"' . implode('","', $ids) . '"' . ");";
131 + $query = "UPDATE `" . $this->getFullTableName($table) . "` SET " . substr($cases, 0, -2) . " WHERE `$index` IN(" . implode(",", $ids) . ");";
108 132
109 133 }
110 134
111 135 return $this->db->query($query);
@@ -143,9 +167,9 @@
143 167 public function updateWithTwoIndex(Model $table, array $values, ?string $index = null, ?string $index2 = null, bool $raw = false)
144 168 {
145 169 $final = [];
146 170 $ids = [];
147 - $driver = $table->getConnection()->getDriverName();
171 + $driver = $table->getConnection()->getName();
148 172
149 173 if (!count($values)) {
150 174 return false;
151 175 }
@@ -153,20 +177,35 @@
153 177 if (!isset($index) || empty($index)) {
154 178 $index = $table->getKeyName();
155 179 }
156 180
181 + // Identifiers are interpolated as raw SQL below — reject anything that is
182 + // not a plain column name so an attacker-supplied key cannot inject SQL.
183 + $this->assertIdentifier($index);
184 + $this->assertIdentifier($index2);
185 +
157 186 foreach ($values as $key => $val) {
158 - $ids[] = $val[$index];
159 - $ids2[] = $val[$index2];
187 + $id1 = $this->db->prepare('%s', $val[$index]);
188 + $id2 = $this->db->prepare('%s', $val[$index2]);
189 + $ids[] = $id1;
190 + $ids2[] = $id2;
160 191 foreach (array_keys($val) as $field) {
161 192 if ($field !== $index || $field !== $index2) {
162 - $finalField = $raw ? Common::mysqlEscape($val[$field]) : "'" . Common::mysqlEscape($val[$field]) . "'";
163 - $value = (is_null($val[$field]) ? 'NULL' : $finalField);
193 + $this->assertIdentifier($field);
194 + // prepare('%s', ...) quotes and escapes; Common::mysqlEscape()
195 + // is unsafe for JSON-valid scalar strings. $raw is internal-only.
196 + if (is_null($val[$field])) {
197 + $value = 'NULL';
198 + } elseif ($raw) {
199 + $value = Common::mysqlEscape($val[$field]);
200 + } else {
201 + $value = $this->prepareScalar($table, $val[$field]);
202 + }
164 203
165 204 if (Common::disableBacktick($driver)) {
166 - $final[$field][] = 'WHEN (' . $index . ' = \'' . Common::mysqlEscape($val[$index]) . '\' AND ' . $index2 . ' = \'' . $val[$index2] . '\') THEN ' . $value . ' ';
205 + $final[$field][] = 'WHEN (' . $index . ' = ' . $id1 . ' AND ' . $index2 . ' = ' . $id2 . ') THEN ' . $value . ' ';
167 206 } else {
168 - $final[$field][] = 'WHEN (`' . $index . '` = "' . Common::mysqlEscape($val[$index]) . '" AND `' . $index2 . '` = "' . $val[$index2] . '") THEN ' . $value . ' ';
207 + $final[$field][] = 'WHEN (`' . $index . '` = ' . $id1 . ' AND `' . $index2 . '` = ' . $id2 . ') THEN ' . $value . ' ';
169 208 }
170 209 }
171 210 }
172 211 }
@@ -178,9 +217,9 @@
178 217 $cases .= '"' . $k . '" = (CASE ' . implode("\n", $v) . "\n"
179 218 . 'ELSE "' . $k . '" END), ';
180 219 }
181 220
182 - $query = "UPDATE \"" . $this->getFullTableName($table) . '" SET ' . substr($cases, 0, -2) . " WHERE \"$index\" IN('" . implode("','", $ids) . "') AND \"$index2\" IN('" . implode("','", $ids2) . "');";
221 + $query = "UPDATE \"" . $this->getFullTableName($table) . '" SET ' . substr($cases, 0, -2) . " WHERE \"$index\" IN(" . implode(",", $ids) . ") AND \"$index2\" IN(" . implode(",", $ids2) . ");";
183 222 } else {
184 223 $cases = '';
185 224 foreach ($final as $k => $v) {
186 225 $cases .= '`' . $k . '` = (CASE ' . implode("\n", $v) . "\n"
@@ -185,9 +224,9 @@
185 224 foreach ($final as $k => $v) {
186 225 $cases .= '`' . $k . '` = (CASE ' . implode("\n", $v) . "\n"
187 226 . 'ELSE `' . $k . '` END), ';
188 227 }
189 - $query = "UPDATE `" . $this->getFullTableName($table) . "` SET " . substr($cases, 0, -2) . " WHERE `$index` IN(" . '"' . implode('","', $ids) . '")' . " AND `$index2` IN(" . '"' . implode('","', $ids2) . '"' . " );";
228 + $query = "UPDATE `" . $this->getFullTableName($table) . "` SET " . substr($cases, 0, -2) . " WHERE `$index` IN(" . implode(",", $ids) . ")" . " AND `$index2` IN(" . implode(",", $ids2) . ");";
190 229 }
191 230 return $this->db->query($query);
192 231 }
193 232
@@ -199,6 +238,52 @@
199 238 */
200 239 private function getFullTableName(Model $model): string
201 240 {
202 241 return $this->db->prefix . $model->getTable();
242 + }
243 +
244 + /**
245 + * Guard a value that is about to be used as a raw SQL column/index identifier.
246 + *
247 + * Identifiers cannot be bound as parameters, so any key that is interpolated
248 + * between backticks must be a plain column name. A value carrying a backtick
249 + * (or anything outside [A-Za-z0-9_]) would break out of the identifier and
250 + * inject SQL, so we fail closed rather than escape — a legitimate column name
251 + * always matches.
252 + *
253 + * @param string $identifier
254 + * @return void
255 + * @throws \InvalidArgumentException
256 + */
257 + private function assertIdentifier($identifier): void
258 + {
259 + if (!is_string($identifier) || !preg_match('/^[A-Za-z0-9_]+$/', $identifier)) {
260 + throw new \InvalidArgumentException('Invalid column identifier in batch update.');
261 + }
262 + }
263 +
264 + /**
265 + * Normalize a non-null scalar value, then bind it via prepare().
266 + *
267 + * prepare('%s', ...) only accepts scalars: a DateTime or a stringable object
268 + * would become an empty string. Callers legitimately pass DateTime objects
269 + * (e.g. updated_at, refunded_at) and boolean flags, which the previous
270 + * string-concatenation path rendered via __toString()/(int) cast. Reproduce
271 + * that normalization before binding so stored values are unchanged.
272 + *
273 + * @param Model $table
274 + * @param mixed $input non-null value
275 + * @return string prepared, quoted SQL literal
276 + */
277 + private function prepareScalar(Model $table, $input): string
278 + {
279 + if ($input instanceof \DateTimeInterface) {
280 + $input = $input->format($table->getDateFormat());
281 + } elseif (is_bool($input)) {
282 + $input = (int) $input;
283 + } elseif (is_object($input) && method_exists($input, '__toString')) {
284 + $input = (string) $input;
285 + }
286 +
287 + return $this->db->prepare('%s', $input);
203 288 }
204 289 }