Skip to content

[🐛 Bug]: By.name() runs String.format twice; a '%' in the name throws or corrupts the CSS fallback selector #17807

Description

@haskiindahouse

Title: [🐛 Bug]: By.name() runs String.format twice; a '%' in the name throws or corrupts the CSS fallback selector


Description

By.name(String) runs String.format twice over user input. A name containing % either throws a java.util.*FormatException at construction time or produces a wrong CSS fallback selector that locates a different element, with no error raised.

Two lines interact to cause this:

  • By.java:245: ByName pre-substitutes the name into the CSS pattern with String.format("*[name='%s']", name.replace("'", "\\'")) and passes the result up as the formatString argument to PreW3CLocator. Any % from the user's name now sits inside a format string.
  • By.java:427: the PreW3CLocator constructor runs a second String.format(formatString, cssEscape(value)) over that string.

By.id ("#%s") and By.className (".%s") pass an untouched literal format string plus the value separately, so they format once. ByName is the only locator that pre-formats the value into the format string.

Observed against selenium-api 4.46.0, present on trunk:

By.name(...) Result
"foo" OK → *[name='foo']
"100%" throws java.util.UnknownFormatConversionException: Conversion = ''' (the trailing %' reads as a format conversion)
"50%off" throws java.util.IllegalFormatConversionException: o != java.lang.String (%o conversion, String arg)
"a%db" throws java.util.IllegalFormatConversionException: d != java.lang.String
"n%me" throws java.util.UnknownFormatConversionException: Conversion = 'm'
"foo%sbar" builds without error, corrupt fallback CSS *[name='foofoo\%sbarbar'] (prefix/suffix doubled, wrong target)

The doubling for foo%sbar: the %s inside the pre-formatted string *[name='foo%sbar'] gets replaced by the escaped value foo\%sbar in the second format pass, yielding *[name='foo + foo\%sbar + bar'].

HTML permits % in a name attribute (e.g. name="50%off"), so ordinary input reaches this path.

Reproducible Code

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

public class RealByTest {
  // Pull the ByCssSelector 'fallback' PreW3CLocator built, and its cssSelector string,
  // to see the ACTUAL selector the shipped library produced.
  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(); }
    }
    if (f == null) return "<no fallback field>";
    f.setAccessible(true);
    Object fb = f.get(by);
    Field cf = fb.getClass().getDeclaredField("cssSelector");
    cf.setAccessible(true);
    return (String) cf.get(fb);
  }

  public static void main(String[] args) {
    for (String n : new String[]{"foo", "100%", "foo%sbar", "50%off"}) {
      System.out.print("By.name(\"" + n + "\") -> ");
      try {
        By by = By.name(n);
        System.out.println("built OK; fallback CSS=" + fallbackSelector(by));
      } catch (Throwable t) {
        System.out.println("THROWS " + t.getClass().getName() + ": " + t.getMessage());
      }
    }
  }
}

Run against the real jar; no browser needed to observe the construction-time throw and the corrupted selector:

javac -cp selenium-api-4.46.0.jar -d . RealByTest.java
java  -cp selenium-api-4.46.0.jar:. RealByTest

Output:

By.name("foo")     -> built OK; fallback CSS=*[name='foo']
By.name("100%")    -> THROWS java.util.UnknownFormatConversionException: Conversion = '''
By.name("foo%sbar")-> built OK; fallback CSS=*[name='foofoo\%sbarbar']
By.name("50%off")  -> THROWS java.util.IllegalFormatConversionException: o != java.lang.String

Cross-check with an independent CSS engine (jsdom): the correct selector *[name='50%off'] matches the intended element while Selenium throws (0 elements). For foo%sbar, the intended *[name='foo%sbar'] matches element b; Selenium's *[name='foofoo\%sbarbar'] matches a different element ab. The wrong-target case produces no error anywhere.

Expected vs Actual

  • Expected: By.name("50%off") builds a fallback selector that matches an element with name="50%off"; % in a name is a literal character.
  • Actual: construction throws UnknownFormat/IllegalFormatConversionException, or (for names containing sequences like %s that happen to be valid conversions) produces a corrupted selector that locates the wrong element.

Root cause (file:line, trunk)

  • java/src/org/openqa/selenium/By.java:245: ByName pre-formats the value into the pattern and passes the result as formatString.
  • java/src/org/openqa/selenium/By.java:427: PreW3CLocator re-runs String.format(formatString, cssEscape(value)) over it.

Suggested fix

Make ByName behave like ById/ByClassName: pass a plain literal format string and let PreW3CLocator do the single format pass over the escaped value.

public ByName(String name) {
  super(
      "name",
      Require.argument("Name", name).nonNull("Cannot find elements when name text is null."),
      "*[name='%s']");           // literal format string, no pre-format
  this.name = name;
}

The existing cssEscape(value) in PreW3CLocator then handles the quoting (' is already in the CSS_ESCAPE character class, so the manual name.replace("'", "\\'") becomes redundant). This formats once and treats % as a literal. A regression test over names containing %, %s, %d, %o, %' should accompany the fix.


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; tested on Linux/macOS)
  • Language Binding: Java
  • Browsers: N/A (reproduces at selector-construction time, 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