diff --git a/includes/database/database.inc b/includes/database/database.inc index 4539b37a74db56cb0f30c487d51e23288b6d6801..4cc1a33d75565d11302f02117687b8113690c8b0 100644 --- a/includes/database/database.inc +++ b/includes/database/database.inc @@ -540,6 +540,63 @@ public function makeSequenceName($table, $field) { return $this->prefixTables('{' . $table . '}_' . $field . '_seq'); } + /** + * Flatten an array of query comments into a single comment string. + * + * The comment string will be sanitized to avoid SQL injection attacks. + * + * @param $comments + * An array of query comment strings. + * + * @return + * A sanitized comment string. + */ + public function makeComment($comments) { + if (empty($comments)) + return ''; + + // Flatten the array of comments. + $comment = implode('; ', $comments); + + // Sanitize the comment string so as to avoid SQL injection attacks. + return '/* ' . $this->filterComment($comment) . ' */ '; + } + + /** + * Sanitize a query comment string. + * + * Ensure a query comment does not include strings such as "* /" that might + * terminate the comment early. This avoids SQL injection attacks via the + * query comment. The comment strings in this example are separated by a + * space to avoid PHP parse errors. + * + * For example, the comment: + * @code + * db_update('example') + * ->condition('id', $id) + * ->fields(array('field2' => 10)) + * ->comment('Exploit * / DROP TABLE node; --') + * ->execute() + * @endcode + * + * Would result in the following SQL statement being generated: + * @code + * "/ * Exploit * / DROP TABLE node; -- * / UPDATE example SET field2=..." + * @endcode + * + * Unless the comment is sanitised first, the SQL server would drop the + * node table and ignore the rest of the SQL statement. + * + * @param $comment + * A query comment string. + * + * @return + * A sanitized version of the query comment string. + */ + protected function filterComment($comment = '') { + return preg_replace('/(\/\*\s*)|(\s*\*\/)/', '', $comment); + } + /** * Executes a query string against the database. * diff --git a/includes/database/mysql/query.inc b/includes/database/mysql/query.inc index f7fb52f04595ee2c43dcf1afda96fe2a8e39ba2a..888b6a5a450e613b172814e53b0f53f27e76cf69 100644 --- a/includes/database/mysql/query.inc +++ b/includes/database/mysql/query.inc @@ -42,8 +42,8 @@ public function execute() { } public function __toString() { - // Create a comments string to prepend to the query. - $comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : ''; + // Create a sanitized comment string to prepend to the query. + $comments = $this->connection->makeComment($this->comments); // Default fields are always placed first for consistency. $insert_fields = array_merge($this->defaultFields, $this->insertFields); @@ -92,8 +92,8 @@ public function __toString() { // not transactional, and result in an implicit COMMIT. When we are in a // transaction, fallback to the slower, but transactional, DELETE. if ($this->connection->inTransaction()) { - // Create a comments string to prepend to the query. - $comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : ''; + // Create a comment string to prepend to the query. + $comments = $this->connection->makeComment($this->comments); return $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '}'; } else { diff --git a/includes/database/pgsql/query.inc b/includes/database/pgsql/query.inc index fe7909e17c5db9b8e4c310761a4bc46596cb9442..f3783a9ca8f9ceb4f745c650fa826ee3271e1df4 100644 --- a/includes/database/pgsql/query.inc +++ b/includes/database/pgsql/query.inc @@ -103,8 +103,8 @@ public function execute() { } public function __toString() { - // Create a comments string to prepend to the query. - $comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : ''; + // Create a sanitized comment string to prepend to the query. + $comments = $this->connection->makeComment($this->comments); // Default fields are always placed first for consistency. $insert_fields = array_merge($this->defaultFields, $this->insertFields); diff --git a/includes/database/query.inc b/includes/database/query.inc index 7f3e9ff85703eb77978013bd3d2d9c656e0939b7..23b652f9b4b439114d677db0ddd5f0a75414814a 100644 --- a/includes/database/query.inc +++ b/includes/database/query.inc @@ -361,6 +361,9 @@ public function nextPlaceholder() { * for easier debugging and allows you to more easily find where a query * with a performance problem is being generated. * + * The comment string will be sanitized to remove * / and other characters + * that may terminate the string early so as to avoid SQL injection attacks. + * * @param $comment * The comment string to be inserted into the query. * @@ -623,9 +626,8 @@ public function execute() { * The prepared statement. */ public function __toString() { - - // Create a comments string to prepend to the query. - $comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : ''; + // Create a sanitized comment string to prepend to the query. + $comments = $this->connection->makeComment($this->comments); // Default fields are always placed first for consistency. $insert_fields = array_merge($this->defaultFields, $this->insertFields); @@ -815,9 +817,8 @@ public function execute() { * The prepared statement. */ public function __toString() { - - // Create a comments string to prepend to the query. - $comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : ''; + // Create a sanitized comment string to prepend to the query. + $comments = $this->connection->makeComment($this->comments); $query = $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '} '; @@ -884,8 +885,8 @@ public function execute() { * The prepared statement. */ public function __toString() { - // Create a comments string to prepend to the query. - $comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : ''; + // Create a sanitized comment string to prepend to the query. + $comments = $this->connection->makeComment($this->comments); return $comments . 'TRUNCATE {' . $this->connection->escapeTable($this->table) . '} '; } @@ -1111,9 +1112,8 @@ public function execute() { * The prepared statement. */ public function __toString() { - - // Create a comments string to prepend to the query. - $comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : ''; + // Create a sanitized comment string to prepend to the query. + $comments = $this->connection->makeComment($this->comments); // Expressions take priority over literal fields, so we process those first // and remove any literal fields that conflict. diff --git a/includes/database/select.inc b/includes/database/select.inc index 6e4b0dc4869ae3915d4e9a2ca4f6c74665c5641f..716c2fc3dbf7f67f82ac361e2c61e2694ddf93bb 100644 --- a/includes/database/select.inc +++ b/includes/database/select.inc @@ -1439,9 +1439,8 @@ public function countQuery() { } public function __toString() { - - // Create a comments string to prepend to the query. - $comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : ''; + // Create a sanitized comment string to prepend to the query. + $comments = $this->connection->makeComment($this->comments); // SELECT $query = $comments . 'SELECT '; diff --git a/includes/database/sqlite/query.inc b/includes/database/sqlite/query.inc index a176ed64937c90e9e13ce7a680a2d0ddefefd9d9..6b8a72f2ab46aed8da9f908a7f66f4f71d27b11a 100644 --- a/includes/database/sqlite/query.inc +++ b/includes/database/sqlite/query.inc @@ -32,8 +32,8 @@ public function execute() { } public function __toString() { - // Create a comments string to prepend to the query. - $comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : ''; + // Create a sanitized comment string to prepend to the query. + $comments = $this->connection->makeComment($this->comments); // Produce as many generic placeholders as necessary. $placeholders = array_fill(0, count($this->insertFields), '?'); @@ -148,8 +148,8 @@ public function execute() { */ class TruncateQuery_sqlite extends TruncateQuery { public function __toString() { - // Create a comments string to prepend to the query. - $comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : ''; + // Create a sanitized comment string to prepend to the query. + $comments = $this->connection->makeComment($this->comments); return $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '} '; } diff --git a/modules/simpletest/tests/database_test.test b/modules/simpletest/tests/database_test.test index 231355ceb1d45fff45311381fe1191505f9cead0..c22d1fc5d0b28600d1e5f044a87795c5e03b0758 100644 --- a/modules/simpletest/tests/database_test.test +++ b/modules/simpletest/tests/database_test.test @@ -1324,6 +1324,27 @@ class DatabaseSelectTestCase extends DatabaseTestCase { $this->assertEqual($query, $expected, t('The flattened query contains the comment string.')); } + /** + * Test query COMMENT system against vulnerabilities. + */ + function testVulnerableComment() { + $query = db_select('test')->comment('Testing query comments */ SELECT nid FROM {node}; --'); + $name_field = $query->addField('test', 'name'); + $age_field = $query->addField('test', 'age', 'age'); + $result = $query->execute(); + + $num_records = 0; + foreach ($result as $record) { + $num_records++; + } + + $query = (string)$query; + $expected = "/* Testing query comments SELECT nid FROM {node}; -- */ SELECT test.name AS name, test.age AS age\nFROM \n{test} test"; + + $this->assertEqual($num_records, 4, t('Returned the correct number of rows.')); + $this->assertEqual($query, $expected, t('The flattened query contains the sanitised comment string.')); + } + /** * Test basic conditionals on SELECT statements. */