Security advisory GHSA-ph98-v78f-jqrm: SQL injection in jackalope/jackalope-doctrine-dbal
Package: jackalope/jackalope-doctrine-dbal (composer), vulnerable version: 1.7.3, fixed in: 1.7.4
CWE: CWE-89
Files changed by the fix:
src/Jackalope/Transport/DoctrineDBAL/Query/QOMWalker.php

Fix diff:
diff --git a/src/Jackalope/Transport/DoctrineDBAL/Query/QOMWalker.php b/src/Jackalope/Transport/DoctrineDBAL/Query/QOMWalker.php
index f3d945c7..70e630fd 100644
--- a/src/Jackalope/Transport/DoctrineDBAL/Query/QOMWalker.php
+++ b/src/Jackalope/Transport/DoctrineDBAL/Query/QOMWalker.php
@@ -385,28 +385,28 @@ public function walkJoinSource(QOM\JoinInterface $source, $root = true)
 
         switch ($source->getJoinType()) {
             case QOM\QueryObjectModelConstantsInterface::JCR_JOIN_TYPE_INNER:
-                $sql .= "INNER JOIN phpcr_nodes $rightAlias ";
+                $sql .= sprintf("INNER JOIN phpcr_nodes %s ", $rightAlias);
                 break;
             case QOM\QueryObjectModelConstantsInterface::JCR_JOIN_TYPE_LEFT_OUTER:
-                $sql .= "LEFT JOIN phpcr_nodes $rightAlias ";
+                $sql .= sprintf("LEFT JOIN phpcr_nodes %s ", $rightAlias);
                 break;
             case QOM\QueryObjectModelConstantsInterface::JCR_JOIN_TYPE_RIGHT_OUTER:
-                $sql .= "RIGHT JOIN phpcr_nodes $rightAlias ";
+                $sql .= sprintf("RIGHT JOIN phpcr_nodes %s ", $rightAlias);
                 break;
         }
 
-        $sql .= "ON ( $leftAlias.workspace_name = $rightAlias.workspace_name AND $nodeTypeClause ";
+        $sql .= sprintf("ON ( %s.workspace_name = %s.workspace_name AND %s ", $leftAlias, $rightAlias, $nodeTypeClause);
         $sql .= 'AND ' . $this->walkJoinCondition($source->getLeft(), $source->getRight(), $source->getJoinCondition()) . ' ';
         $sql .= ') '; // close on-clause
 
 
         if ($root) { // The method call is not recursed when $root is true, so we can add a WHERE clause
             // TODO: revise this part for alternatives
-            $sql .= "WHERE $leftAlias.workspace_name = ? AND $leftAlias.type IN ('" . $left->getNodeTypeName() . "'";
+            $sql .= sprintf("WHERE %s.workspace_name = ? AND %s.type IN ('%s'", $leftAlias, $leftAlias, $left->getNodeTypeName());
             $subTypes = $this->nodeTypeManager->getSubtypes($left->getNodeTypeName());
             foreach ($subTypes as $subType) {
                 /* @var $subType NodeTypeInterface */
-                $sql .= ", '" . $subType->getName() . "'";
+                $sql .= sprintf(", '%s'", $subType->getName());
             }
             $sql .= ')';
         }
@@ -455,7 +455,7 @@ public function walkChildNodeJoinCondition(QOM\ChildNodeJoinConditionInterface $
         $leftAlias = $this->getTableAlias($condition->getParentSelectorName());
         $concatExpression = $this->platform->getConcatExpression("$leftAlias.path", "'/%'");
 
-        return "($rightAlias.path LIKE " . $concatExpression . " AND $rightAlias.depth = $leftAlias.depth + 1) ";
+        return sprintf("(%s.path LIKE %s AND %s.depth = %s.depth + 1) ", $rightAlias, $concatExpression, $rightAlias, $leftAlias);
     }
 
     /**
@@ -469,7 +469,7 @@ public function walkDescendantNodeJoinCondition(QOM\DescendantNodeJoinConditionI
         $leftAlias = $this->getTableAlias($condition->getAncestorSelectorName());
         $concatExpression = $this->platform->getConcatExpression("$leftAlias.path", "'/%'");
 
-        return "$rightAlias.path LIKE " . $concatExpression . " ";
+        return sprintf("%s.path LIKE %s ", $rightAlias, $concatExpression);
     }
 
     /**
@@ -521,7 +521,7 @@ public function walkConstraint(QOM\ConstraintInterface $constraint)
             return $this->walkFullTextSearchConstraint($constraint);
         }
 
-        throw new InvalidQueryException("Constraint " . get_class($constraint) . " not yet supported.");
+        throw new InvalidQueryException(sprintf("Constraint %s not yet supported.", get_class($constraint)));
     }
 
     /**
@@ -531,7 +531,11 @@ public function walkConstraint(QOM\ConstraintInterface $constraint)
      */
     public function walkSameNodeConstraint(QOM\SameNodeInterface $constraint)
     {
-        return $this->getTableAlias($constraint->getSelectorName()) . ".path = '" . $constraint->getPath() . "'";
+        return sprintf(
+            "%s.path = '%s'",
+            $this->getTableAlias($constraint->getSelectorName()),
+            $constraint->getPath()
+        );
     }
 
     /**
@@ -541,7 +545,10 @@ public function walkSameNodeConstraint(QOM\SameNodeInterface $constraint)
      */
     public function walkFullTextSearchConstraint(QOM\FullTextSearchInterface $constraint)
     {
-        return $this->sqlXpathExtractValue($this->getTableAlias($constraint->getSelectorName()), $constraint->getPropertyName()).' LIKE '. $this->conn->quote('%'.$constraint->getFullTextSearchExpression().'%');
+        return sprintf('%s LIKE %s',
+            $this->sqlXpathExtractValue($this->getTableAlias($constraint->getSelectorName()), $constraint->getPropertyName()),
+            $this->conn->quote('%'.$constraint->getFullTextSearchExpression().'%')
+        );
     }
 
     /**
@@ -568,7 +575,11 @@ public function walkDescendantNodeConstraint(QOM\DescendantNodeInterface $constr
             throw new InvalidQueryException("Trailing slash in $ancestorPath");
         }
 
-        return $this->getTableAlias($constraint->getSelectorName()) . ".path LIKE '" . $ancestorPath . "/%'";
+        return sprintf(
+            "%s.path LIKE '%s/%%'",
+            $this->getTableAlias($constraint->getSelectorName()),
+            addcslashes($ancestorPath, "'")
+        );
     }
 
     /**
@@ -578,7 +589,11 @@ public function walkDescendantNodeConstraint(QOM\DescendantNodeInterface $constr
      */
     public function walkChildNodeConstraint(QOM\ChildNodeInterface $constraint)
     {
-        return $this->getTableAlias($constraint->getSelectorName()) . ".parent = '" . $constraint->getParentPath() . "'";
+        return sprintf(
+            "%s.parent = '%s'",
+            $this->getTableAlias($constraint->getSelectorName()),
+            addcslashes($constraint->getParentPath(), "'")
+        );
     }
 
     /**
@@ -588,7 +603,11 @@ public function walkChildNodeConstraint(QOM\ChildNodeInterface $constraint)
      */
     public function walkAndConstraint(QOM\AndInterface $constraint)
     {
-        return "(" . $this->walkConstraint($constraint->getConstraint1()) . " AND " . $this->walkConstraint($constraint->getConstraint2()) . ")";
+        return sprintf(
+            "(%s AND %s)",
+            $this->walkConstraint($constraint->getConstraint1()),
+            $this->walkConstraint($constraint->getConstraint2())
+        );
     }
 
     /**
@@ -598,7 +617,11 @@ public function walkAndConstraint(QOM\AndInterface $constraint)
      */
     public function walkOrConstraint(QOM\OrInterface $constraint)
     {
-        return "(" . $this->walkConstraint($constraint->getConstraint1()) . " OR " . $this->walkConstraint($constraint->getConstraint2()) . ")";
+        return sprintf(
+            "(%s OR %s)",
+            $this->walkConstraint($constraint->getConstraint1()),
+            $this->walkConstraint($constraint->getConstraint2())
+        );
     }
 
     /**
@@ -608,7 +631,10 @@ public function walkOrConstraint(QOM\OrInterface $constraint)
      */
     public function walkNotConstraint(QOM\NotInterface $constraint)
     {
-        return "NOT (" . $this->walkConstraint($constraint->getConstraint()) . ")";
+        return sprintf(
+            "NOT (%s)",
+            $this->walkConstraint($constraint->getConstraint())
+        );
     }
 
     /**
@@ -670,9 +696,16 @@ public function walkComparisonConstraint(QOM\ComparisonInterface $constraint)
                     $literal = implode(':', $parts);
                 }
 
-                return $this->platform->getConcatExpression("$alias.namespace", "(CASE $alias.namespace WHEN '' THEN '' ELSE ':' END)", "$alias.local_name") . " " .
-                    $operator . " " .
-                    $this->conn->quote($literal);
+                return sprintf(
+                    '%s %s %s',
+                    $this->platform->getConcatExpression(
+                        sprintf("%s.namespace", $alias),
+                        sprintf("(CASE %s.namespace WHEN '' THEN '' ELSE ':' END)", $alias),
+                        sprintf("%s.local_name", $alias)
+                    ),
+                    $operator,
+                    $this->conn->quote($literal)
+                ) ;
             }
 
             if ('jcr:path' !== $operand->getPropertyName() && 'jcr:uuid' !== $operand->getPropertyName()) {
@@ -687,10 +720,12 @@ public function walkComparisonConstraint(QOM\ComparisonInterface $constraint)
             }
         }
 
-        return
-            $this->walkOperand($operator1) . ' ' .
-            $operator . ' ' .
-            $this->walkOperand($operator2);
+        return sprintf(
+            '%s %s %s',
+            $this->walkOperand($operator1),
+            $operator,
+            $this->walkOperand($operator2)
+        );
     }
 
     /**
@@ -723,10 +758,10 @@ public function walkNumComparisonConstraint(QOM\PropertyValueInterface $property
 
         if ($this->platform instanceof MySQLPlatform && '=' === $operator) {
             return sprintf(
-                '0 != FIND_IN_SET("%s", REPLACE(EXTRACTVALUE(%s.props, \'//sv:property[@sv:name="%s"]/sv:value\'), " ", ","))',
+                '0 != FIND_IN_SET("%s", REPLACE(EXTRACTVALUE(%s.props, \'//sv:property[@sv:name=%s]/sv:value\'), " ", ","))',
                 $literalOperand->getLiteralValue(),
                 $alias,
-                $property
+                Xpath::escape($property)
             );
         }
 
@@ -787,14 +822,18 @@ public function walkOperand(QOM\OperandInterface $operand)
             $selectorName = $operand->getSelectorName();
             $alias = $this->getTableAlias($selectorName);
 
-            return $this->platform->getConcatExpression("$alias.namespace", "(CASE $alias.namespace WHEN '' THEN '' ELSE ':' END)", "$alias.local_name");
+            return $this->platform->getConcatExpression(
+                sprintf("%s.namespace", $alias),
+                sprintf("(CASE %s.namespace WHEN '' THEN '' ELSE ':' END)", $alias),
+                sprintf("%s.local_name", $alias)
+            );
         }
 
         if ($operand instanceof QOM\NodeLocalNameInterface) {
             $selectorName = $operand->getSelectorName();
             $alias = $this->getTableAlias($selectorName);
 
-            return "$alias.local_name";
+            return sprintf("%s.local_name", $alias);
         }
 
         if ($operand instanceof QOM\LowerCaseInterface) {
@@ -813,10 +852,10 @@ public function walkOperand(QOM\OperandInterface $operand)
             $alias = $this->getTableAlias($operand->getSelectorName() . '.' . $operand->getPropertyName());
             $property = $operand->getPropertyName();
             if ($property === 'jcr:path') {
-                return "$alias.path";
+                return sprintf("%s.path", $alias);
             }
             if ($property === "jcr:uuid") {
-                return "$alias.identifier";
+                return sprintf("%s.identifier", $alias);
             }
 
             return $this->sqlXpathExtractValue($alias, $property);
@@ -829,7 +868,7 @@ public function walkOperand(QOM\OperandInterface $operand)
             return $this->sqlXpathExtractValueAttribute($alias, $property, 'length');
         }
 
-        throw new InvalidQueryException("Dynamic operand " . get_class($operand) . " not yet supported.");
+        throw new InvalidQueryException(sprintf("Dynamic operand %s not yet supported.", get_class($operand)));
     }
 
     /**
@@ -920,18 +959,18 @@ private function getLiteralValue(QOM\LiteralInterface $operand)
     private function sqlXpathValueExists($alias, $property)
     {
         if ($this->platform instanceof MySQLPlatform) {
-            return "EXTRACTVALUE($alias.props, 'count(//sv:property[@sv:name=\"" . $property . "\"]/sv:value[1])') = 1";
+            return sprintf("EXTRACTVALUE(%s.props, 'count(//sv:property[@sv:name=%s]/sv:value[1])') = 1", $alias, Xpath::escape($property));
         }
 
         if ($this->platform instanceof PostgreSQL94Platform || $this->platform instanceof PostgreSqlPlatform) {
-            return "xpath_exists('//sv:property[@sv:name=\"" . $property . "\"]/sv:value[1]', CAST($alias.props AS xml), ".$this->sqlXpathPostgreSQLNamespaces().") = 't'";
+            return sprintf("xpath_exists('//sv:property[@sv:name=%s]/sv:value[1]', CAST(%s.props AS xml), ".$this->sqlXpathPostgreSQLNamespaces().") = 't'", Xpath::escape($property), $alias);
         }
 
         if ($this->platform instanceof SqlitePlatform) {
-            return "EXTRACTVALUE($alias.props, 'count(//sv:property[@sv:name=\"" . $property . "\"]/sv:value[1])') = 1";
+            return sprintf("EXTRACTVALUE(%s.props, 'count(//sv:property[@sv:name=%s]/sv:value[1])') = 1", $alias, Xpath::escape($property));
         }
 
-        throw new NotImplementedException("Xpath evaluations cannot be executed with '" . $this->platform->getName() . "' yet.");
+        throw new NotImplementedException(sprintf("Xpath evaluations cannot be executed with '%s' yet.", $this->platform->getName()));
     }
 
     /**
@@ -945,44 +984,44 @@ private function sqlXpathValueExists($alias, $property)
     private function sqlXpathExtractValue($alias, $property, $column = 'props')
     {
         if ($this->platform instanceof MySQLPlatform) {
-            return "EXTRACTVALUE($alias.$column, '//sv:property[@sv:name=\"" . $property . "\"]/sv:value[1]')";
+            return sprintf("EXTRACTVALUE(%s.%s, '//sv:property[@sv:name=%s]/sv:value[1]')", $alias, $column, Xpath::escape($property));
         }
 
         if ($this->platform instanceof PostgreSQL94Platform || $this->platform instanceof PostgreSqlPlatform) {
-            return "(xpath('//sv:property[@sv:name=\"" . $property . "\"]/sv:value[1]/text()', CAST($alias.$column AS xml), ".$this->sqlXpathPostgreSQLNamespaces()."))[1]::text";
+            return sprintf("(xpath('//sv:property[@sv:name=%s]/sv:value[1]/text()', CAST(%s.%s AS xml), %s))[1]::text", Xpath::escape($property), $alias, $column, $this->sqlXpathPostgreSQLNamespaces());
         }
 
         if ($this->platform instanceof SqlitePlatform) {
-            return "EXTRACTVALUE($alias.$column, '//sv:property[@sv:name=\"" . $property . "\"]/sv:value[1]')";
+            return sprintf("EXTRACTVALUE(%s.%s, '//sv:property[@sv:name=%s]/sv:value[1]')", $alias, $column, Xpath::escape($property));
         }
 
-        throw new NotImplementedException("Xpath evaluations cannot be executed with '" . $this->platform->getName() . "' yet.");
+        throw new NotImplementedException(sprintf("Xpath evaluations cannot be executed with '%s' yet.", $this->platform->getName()));
     }
 
     private function sqlXpathExtractNumValue($alias, $property)
     {
         if ($this->platform instanceof PostgreSQL94Platform || $this->platform instanceof PostgreSqlPlatform) {
-            return "(xpath('//sv:property[@sv:name=\"" . $property . "\"]/sv:value[1]/text()', CAST($alias.props AS xml), ".$this->sqlXpathPostgreSQLNamespaces()."))[1]::text::int";
+            return sprintf("(xpath('//sv:property[@sv:name=%s]/sv:value[1]/text()', CAST(%s.props AS xml), %s))[1]::text::int", Xpath::escape($property), $alias, $this->sqlXpathPostgreSQLNamespaces());
         }
 
-        return 'CAST(' . $this->sqlXpathExtractValue($alias, $property) . ' AS DECIMAL)';
+        return sprintf('CAST(%s AS DECIMAL)', $this->sqlXpathExtractValue($alias, $property));
     }
 
     private function sqlXpathExtractValueAttribute($alias, $property, $attribute, $valueIndex = 1)
     {
         if ($this->platform instanceof MySQLPlatform) {
-            return sprintf("EXTRACTVALUE(%s.props, '//sv:property[@sv:name=\"%s\"]/sv:value[%d]/@%s')", $alias, $property, $valueIndex, $attribute);
+            return sprintf("EXTRACTVALUE(%s.props, '//sv:property[@sv:name=%s]/sv:value[%d]/@%s')", $alias, Xpath::escape($property), $valueIndex, $attribute);
         }
 
         if ($this->platform instanceof PostgreSQL94Platform || $this->platform instanceof PostgreSqlPlatform) {
-            return sprintf("CAST((xpath('//sv:property[@sv:name=\"%s\"]/sv:value[%d]/@%s', CAST(%s.props AS xml), %s))[1]::text AS bigint)", $property, $valueIndex, $attribute, $alias, $this->sqlXpathPostgreSQLNamespaces());
+            return sprintf("CAST((xpath('//sv:property[@sv:name=%s]/sv:value[%d]/@%s', CAST(%s.props AS xml), %s))[1]::text AS bigint)", Xpath::escape($property), $valueIndex, $attribute, $alias, $this->sqlXpathPostgreSQLNamespaces());
         }
 
         if ($this->platform instanceof SqlitePlatform) {
-            return sprintf("EXTRACTVALUE(%s.props, '//sv:property[@sv:name=\"%s\"]/sv:value[%d]/@%s')", $alias, $property, $valueIndex, $attribute);
+            return sprintf("EXTRACTVALUE(%s.props, '//sv:property[@sv:name=%s]/sv:value[%d]/@%s')", $alias, Xpath::escape($property), $valueIndex, $attribute);
         }
 
-        throw new NotImplementedException("Xpath evaluations cannot be executed with '" . $this->platform->getName() . "' yet.");
+        throw new NotImplementedException(sprintf("Xpath evaluations cannot be executed with '%s' yet.", $this->platform->getName()));
     }
 
     /**
@@ -1001,15 +1040,15 @@ private function sqlXpathComparePropertyValue($alias, $property, $value, $operat
         $expression = null;
 
         if ($this->platform instanceof MySQLPlatform) {
-            $expression = "EXTRACTVALUE($alias.props, 'count(//sv:property[@sv:name=\"" . $property . "\"]/sv:value[text()%s%s]) > 0')";
+            $expression = sprintf("EXTRACTVALUE(%s.props, 'count(//sv:property[@sv:name=%s]/sv:value[text()%%s%%s]) > 0')", $alias, Xpath::escape($property));
             // mysql does not escape the backslashes for us, while postgres and sqlite do
             $value = Xpath::escapeBackslashes($value);
         } elseif ($this->platform instanceof PostgreSQL94Platform || $this->platform instanceof PostgreSqlPlatform) {
-            $expression = "xpath_exists('//sv:property[@sv:name=\"" . $property . "\"]/sv:value[text()%s%s]', CAST($alias.props AS xml), ".$this->sqlXpathPostgreSQLNamespaces().") = 't'";
+            $expression = sprintf("xpath_exists('//sv:property[@sv:name=%s]/sv:value[text()%s%s]', CAST(%%s.props AS xml), %%s) = 't'", Xpath::escape($property), $alias, $this->sqlXpathPostgreSQLNamespaces());
         } elseif ($this->platform instanceof SqlitePlatform) {
-            $expression = "EXTRACTVALUE($alias.props, 'count(//sv:property[@sv:name=\"" . $property . "\"]/sv:value[text()%s%s]) > 0')";
+            $expression = sprintf("EXTRACTVALUE(%s.props, 'count(//sv:property[@sv:name=%s]/sv:value[text()%%s%%s]) > 0')", $alias, Xpath::escape($property));
         } else {
-            throw new NotImplementedException("Xpath evaluations cannot be executed with '" . $this->platform->getName() . "' yet.");
+            throw new NotImplementedException(sprintf("Xpath evaluations cannot be executed with '%s' yet.", $this->platform->getName()));
         }
 
         return sprintf($expression, $this->walkOperator($operator), Xpath::escape($value));
@@ -1031,12 +1070,12 @@ private function sqlXpathPostgreSQLNamespaces()
      */
     private function sqlNodeTypeClause($alias, QOM\SelectorInterface $source)
     {
-        $sql = "$alias.type IN ('" . $source->getNodeTypeName() ."'";
+        $sql = sprintf("%s.type IN ('%s'", $alias, $source->getNodeTypeName());
 
         $subTypes = $this->nodeTypeManager->getSubtypes($source->getNodeTypeName());
         foreach ($subTypes as $subType) {
             /* @var $subType NodeTypeInterface */
-            $sql .= ", '" . $subType->getName() . "'";
+            $sql .= sprintf(", '%s'", $subType->getName());
         }
         $sql .= ')';
 
diff --git a/tests/Jackalope/Transport/DoctrineDBAL/ClientTest.php b/tests/Jackalope/Transport/DoctrineDBAL/ClientTest.php
index f5b2f6b2..5c57b422 100644
--- a/tests/Jackalope/Transport/DoctrineDBAL/ClientTest.php
+++ b/tests/Jackalope/Transport/DoctrineDBAL/ClientTest.php
@@ -9,9 +9,11 @@
 use Jackalope\Test\FunctionalTestCase;
 use PDO;
 use PHPCR\PropertyType;
+use PHPCR\Query\QOM\QueryObjectModelConstantsInterface;
 use PHPCR\Query\QueryInterface;
 use PHPCR\Util\NodeHelper;
 use PHPCR\Util\PathHelper;
+use PHPCR\Util\QOM\QueryBuilder;
 use PHPCR\ValueFormatException;
 use ReflectionClass;
 
@@ -435,6 +437,7 @@ public function provideOrder()
                         'value' => 'CCC',
                     ],
                 ],
+                'value',
                 'value DESC',
                 ['three', 'two', 'one'],
             ],
@@ -453,6 +456,7 @@ public function provideOrder()
                     ],
                 ],
                 'value',
+                'value',
                 ['three', 'two', 'one'],
             ],
 
@@ -470,6 +474,7 @@ public function provideOrder()
                     ],
                 ],
                 'value',
+                'value',
                 ['one', 'three', 'two'],
             ],
 
@@ -487,6 +492,7 @@ public function provideOrder()
                     ],
                 ],
                 'value',
+                'value',
                 ['two', 'three', 'one'],
             ],
 
@@ -510,16 +516,71 @@ public function provideOrder()
                         'value' => 5.05,
                     ],
                 ],
+                'value',
                 'title, value ASC',
                 ['two', 'one', 'four', 'three'],
             ],
+
+            // property with double quotes
+            [
+                [
+                    'one' => [
+                        'val"ue' => 'AAA',
+                    ],
+                    'two' => [
+                        'val"ue' => 'BBB',
+                    ],
+                    'three' => [
+                        'val"ue' => 'CCC',
+                    ],
+                ],
+                'val"ue',
+                'val"ue DESC',
+                ['three', 'two', 'one'],
+            ],
+
+            // property with single quotes
+            [
+                [
+                    'one' => [
+                        'val\'ue' => 'AAA',
+                    ],
+                    'two' => [
+                        'val\'ue' => 'BBB',
+                    ],
+                    'three' => [
+                        'val\'ue' => 'CCC',
+                    ],
+                ],
+                'val\'ue',
+                'val\'ue DESC',
+                ['three', 'two', 'one'],
+            ],
+
+            // property with semicolon quotes
+            [
+                [
+                    'one' => [
+                        'val;ue' => 'AAA',
+                    ],
+                    'two' => [
+                        'val;ue' => 'BBB',
+                    ],
+                    'three' => [
+                        'val;ue' => 'CCC',
+                    ],
+                ],
+                'val;ue',
+                'val;ue DESC',
+                ['three', 'two', 'one'],
+            ],
         ];
     }
 
     /**
      * @dataProvider provideOrder
      */
-    public function testOrder($nodes, $orderBy, $expectedOrder)
+    public function testOrder($nodes, $propertyName, $orderBy, $expectedOrder)
     {
         $rootNode = $this->session->getNode('/');
 
@@ -533,7 +594,30 @@ public function testOrder($nodes, $orderBy, $expectedOrder)
         $this->session->save();
 
         $qm = $this->session->getWorkspace()->getQueryManager();
-        $query = $qm->createQuery('SELECT * FROM [nt:unstructured] WHERE value IS NOT NULL ORDER BY ' . $orderBy, QueryInterface::JCR_SQL2);
+        $qf = $qm->getQOMFactory();
+        $qb = new QueryBuilder($qf);
+        $qb->from(
+            $qb->qomf()->selector('a', 'nt:unstructured')
+        );
+        $qb->where($qf->comparison(
+            $qf->propertyValue('a', $propertyName),
+            QueryObjectModelConstantsInterface::JCR_OPERATOR_NOT_EQUAL_TO,
+            $qf->literal('NULL')
+        ));
+
+        $orderBys = explode(',', $orderBy);
+        foreach ($orderBys as $orderByItem) {
+            $orderByParts = explode(' ', trim($orderByItem));
+            $propertyName = $orderByParts[0];
+            $order = isset($orderByParts[1]) ? $orderByParts[1] : 'ASC';
+
+            $qb->addOrderBy(
+                $qb->qomf()->propertyValue('a', $propertyName),
+                $order
+            );
+        }
+
+        $query = $qb->getQuery();
         $result = $query->execute();
 
         $rows = $result->getRows();