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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions