Skip to content

[🐛 Bug]: By.className()/By.id() escape a leading non-ASCII Unicode digit to the wrong ASCII code point, colliding with a different class name #17805

Description

@haskiindahouse

Title: [🐛 Bug]: By.className()/By.id() escape a leading non-ASCII Unicode digit to the wrong ASCII code point, colliding with a different class name


Description

PreW3CLocator.cssEscape (used by By.className and By.id) special-cases a leading digit with Character.isDigit(...), which is true for every Unicode Nd (decimal digit) code point, not only ASCII 09. It then rewrites that first character as "\\" + (30 + Integer.parseInt(first)) + " ": the decimal number 30 + value, printed as text, read back as a hex escape by CSS. That round-trips for ASCII U+0030U+0039 only. A non-ASCII digit of numeric value d gets rewritten to the escape for the ASCII digit U+003d, changing the class/id to a different string.

Result: a leading Arabic-Indic ٥ (U+0665, value 5) escapes to \35 , the escape for ASCII 5, so By.className("٥foo") produces the exact same CSS selector as By.className("5foo"). In the browser this matches a different element, or nothing. No error is raised at any point.

Observed against selenium-api 4.46.0, present on trunk:

Input Selenium fallback CSS Should escape to
By.className("5foo") (ASCII 5) .\35 foo .\35 foo
By.className("٥foo") (U+0665, Arabic-Indic 5) .\35 foo .\665 foo (the U+0665 code point)
By.className("9bar") (U+FF19, fullwidth 9) .\39 bar .\ff19 bar
By.className("৭x") (U+09ED, Bengali 7) .\37 x .\9ed x

Collision proof: By.className("٥foo") and By.className("5foo") produce the identical selector .\35 foo; the non-ASCII digit is indistinguishable from ASCII 5 after escaping.

By.id (format string #%s) goes through the same cssEscape, so it has the same defect. A byte-identical copy of the routine also lives in W3CHttpCommandCodec.java (see below).

JDK behavior (confirmed on JDK 24): Character.isDigit is true for ٥ 9 ৭ ٠; Integer.parseInt("٥") returns 5 without throwing. The guard fires and emits the wrong escape with no exception.

Reproducible Code

import org.openqa.selenium.By;
import java.lang.reflect.Field;

public class RealByClassTest {
  static String fallbackSelector(By by) throws Exception {
    Field f = null; Class<?> cur = by.getClass();
    while (cur != null) {
      try { f = cur.getDeclaredField("fallback"); break; }
      catch (NoSuchFieldException e) { cur = cur.getSuperclass(); }
    }
    f.setAccessible(true);
    Object fb = f.get(by);
    Field cf = fb.getClass().getDeclaredField("cssSelector");
    cf.setAccessible(true);
    return (String) cf.get(fb);
  }

  static String show(String s) {           // print non-ASCII as \uXXXX
    StringBuilder b = new StringBuilder();
    for (int i = 0; i < s.length(); i++) {
      char c = s.charAt(i);
      b.append(c >= 0x20 && c < 0x7f ? String.valueOf(c)
                                     : String.format("\\u%04X", (int) c));
    }
    return b.toString();
  }

  public static void main(String[] args) throws Exception {
    for (String cls : new String[]{"5foo", "٥foo", "9bar", "৭x"}) {
      By by = By.className(cls);
      System.out.printf("By.className(\"%s\") -> %s%n", show(cls), show(fallbackSelector(by)));
    }
    String a = fallbackSelector(By.className("٥foo"));
    String b = fallbackSelector(By.className("5foo"));
    System.out.println("COLLISION '٥foo' vs '5foo' same selector? " + a.equals(b));
  }
}

Run against the real jar; no browser needed to observe the wrong escape and the collision:

javac -encoding UTF-8 -cp selenium-api-4.46.0.jar -d . RealByClassTest.java
java  -Dfile.encoding=UTF-8 -cp selenium-api-4.46.0.jar:. RealByClassTest

Output:

By.className("5foo")     -> .\35 foo
By.className("٥foo") -> .\35 foo
By.className("9bar") -> .\39 bar
By.className("৭x")   -> .\37 x
COLLISION '٥foo' vs '5foo' same selector? true

Cross-check with an independent CSS engine (jsdom): .\35 foo matches the ASCII-5 element (wrong target); the intended .٥foo / .\665 foo match the Arabic-Indic element. With only the ٥foo element present, .\35 foo matches nothing, a false NoSuchElementException.

Expected vs Actual

  • Expected: By.className("٥foo") escapes the leading ٥ (U+0665) to its own code point (.\665 foo) and locates the element whose class starts with ٥.
  • Actual: it escapes to .\35 foo (ASCII 5), colliding with 5foo and matching the wrong element, or nothing. No error surfaces.

Root cause (file:line, trunk)

  • java/src/org/openqa/selenium/By.java:451 (PreW3CLocator.cssEscape):
    if (!using.isEmpty() && Character.isDigit(using.charAt(0))) {
      using = "\\" + (30 + Integer.parseInt(using.substring(0, 1))) + " " + using.substring(1);
    }
    Character.isDigit accepts all Unicode Nd. 30 + Integer.parseInt(...) computes a decimal number (30 + 5 = 35) whose digits CSS then reads as hex text (\35 = U+0035); that coincidence holds for ASCII 09 only.
  • A byte-identical copy exists in java/src/org/openqa/selenium/remote/codec/w3c/W3CHttpCommandCodec.java (cssEscape, line 390); both copies need the fix.

Suggested fix

Gate the escape on ASCII digits and emit the character's own code point. This matches the CSS serialize-an-identifier algorithm, which escapes a leading digit as \ + hex code point + space.

char first = using.charAt(0);
if (first >= '0' && first <= '9') {
  using = "\\3" + first + " " + using.substring(1);   // ASCII digit -> \3X<space>
}
// non-ASCII leading digits need no special digit-escape here; if a broader
// CSS.escape is desired, escape by actual code point, never by numeric value.

The complete route is to implement CSS serialize-an-identifier (or reuse a vetted CSS.escape port) so a leading digit c becomes \ + Integer.toHexString(c) + " ", correct for ASCII and non-ASCII digits alike. Apply the fix to both copies (By.java and W3CHttpCommandCodec.java) with a shared regression test over leading ٥ 9 ৭ ٠.


Other template fields (for the submitter to fill on the form):

  • Selenium version: 4.46.0
  • Regression: Not sure / long-standing (present in current trunk)
  • Operating System: platform-independent (pure Java string handling)
  • Language Binding: Java
  • Browsers: N/A (reproduces at selector-construction time; wrong-target is confirmed with an independent CSS engine, no browser needed)
  • Selenium Grid: No

Found via property-based & differential bug-hunting, part of an effort to scale PBT (DepTyCheck-based) testing across the OSS ecosystem.

If this is intended / by-design: I'm really sorry, please just close it — no need to flag or ban me. I'm trying to scale property-based testing across the whole ecosystem and my publishing agents may have gotten this one wrong. I read every issue and follow up on each.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions