Repository: jhy/jsoup
Pull request #2311 (2025-04-24 16:23:03)

Disallow consecutive combinators in CSS selectors
Rewrite grammar/parser to disallow consecutive combinators in CSS selectors while retaining the non-standard feature of starting a selector with a combinator.


Patch:
diff --git a/src/main/java/org/jsoup/select/QueryParser.java b/src/main/java/org/jsoup/select/QueryParser.java
index d564b66a..ee8604e7 100644
--- a/src/main/java/org/jsoup/select/QueryParser.java
+++ b/src/main/java/org/jsoup/select/QueryParser.java
@@ -55,7 +55,7 @@ public class QueryParser {
      Parse the query. We use this simplified expression of the grammar:
      <pre>
      SelectorGroup   ::= Selector (',' Selector)*
-     Selector        ::= SimpleSequence ( Combinator SimpleSequence )*
+     Selector        ::= [ Combinator ] SimpleSequence ( Combinator SimpleSequence )*
      SimpleSequence  ::= [ TypeSelector ] ( ID | Class | Attribute | Pseudo )*
      Pseudo           ::= ':' Name [ '(' SelectorGroup ')' ]
      Combinator      ::= S+         // descendant (whitespace)
@@ -85,8 +85,16 @@ public class QueryParser {
     }
 
     Evaluator parseSelector() {
-        // SimpleSequence ( Combinator SimpleSequence )*
-        Evaluator left = parseSimpleSequence();
+        // Selector ::= [ Combinator ] SimpleSequence ( Combinator SimpleSequence )*
+        tq.consumeWhitespace();
+
+        Evaluator left;
+        if (tq.matchesAny(Combinators)) {
+            // e.g. query is "> div"; left side is root element
+            left = new StructuralEvaluator.Root();
+        } else {
+            left = parseSimpleSequence();
+        }
 
         while (true) {
             char combinator = 0;
@@ -117,8 +125,6 @@ public class QueryParser {
             left = byTag();
         else if (tq.matchChomp('*'))
             left = new Evaluator.AllElements();
-        else if (tq.matchesAny(Combinators))  // e.g. query is "> div"; type is root element
-            left = new StructuralEvaluator.Root();
 
         // zero or more subclasses (#, ., [)
         while(true) {


Test patch:
diff --git a/src/test/java/org/jsoup/select/QueryParserTest.java b/src/test/java/org/jsoup/select/QueryParserTest.java
index d48c1270..c9addbff 100644
--- a/src/test/java/org/jsoup/select/QueryParserTest.java
+++ b/src/test/java/org/jsoup/select/QueryParserTest.java
@@ -214,4 +214,18 @@ public class QueryParserTest {
             assertThrows(SelectorParseException.class, () -> QueryParser.parse("div:has(p))"));
         assertEquals("Could not parse query 'div:has(p))': unexpected token at ')'", exception.getMessage());
     }
+
+    @Test void consecutiveCombinators() {
+        Selector.SelectorParseException exception1 =
+                assertThrows(Selector.SelectorParseException.class, () -> QueryParser.parse("div>>p"));
+        assertEquals(
+                "Could not parse query 'div>>p': unexpected token at '>p'",
+                exception1.getMessage());
+
+        Selector.SelectorParseException exception2 =
+                assertThrows(Selector.SelectorParseException.class, () -> QueryParser.parse("+ + div"));
+        assertEquals(
+                "Could not parse query '+ + div': unexpected token at '+ div'",
+                exception2.getMessage());
+    }
 }