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.
Title:
[🐛 Bug]: By.name() runs String.format twice; a '%' in the name throws or corrupts the CSS fallback selectorDescription
By.name(String)runsString.formattwice over user input. Anamecontaining%either throws ajava.util.*FormatExceptionat 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:ByNamepre-substitutes the name into the CSS pattern withString.format("*[name='%s']", name.replace("'", "\\'"))and passes the result up as theformatStringargument toPreW3CLocator. Any%from the user'snamenow sits inside a format string.By.java:427: thePreW3CLocatorconstructor runs a secondString.format(formatString, cssEscape(value))over that string.By.id("#%s") andBy.className(".%s") pass an untouched literal format string plus the value separately, so they format once.ByNameis the only locator that pre-formats the value into the format string.Observed against selenium-api 4.46.0, present on
trunk:By.name(...)"foo"*[name='foo']"100%"java.util.UnknownFormatConversionException: Conversion = '''(the trailing%'reads as a format conversion)"50%off"java.util.IllegalFormatConversionException: o != java.lang.String(%oconversion, String arg)"a%db"java.util.IllegalFormatConversionException: d != java.lang.String"n%me"java.util.UnknownFormatConversionException: Conversion = 'm'"foo%sbar"*[name='foofoo\%sbarbar'](prefix/suffix doubled, wrong target)The doubling for
foo%sbar: the%sinside the pre-formatted string*[name='foo%sbar']gets replaced by the escaped valuefoo\%sbarin the second format pass, yielding*[name='foo+foo\%sbar+bar'].HTML permits
%in anameattribute (e.g.name="50%off"), so ordinary input reaches this path.Reproducible Code
Run against the real jar; no browser needed to observe the construction-time throw and the corrupted selector:
Output:
Cross-check with an independent CSS engine (jsdom): the correct selector
*[name='50%off']matches the intended element while Selenium throws (0 elements). Forfoo%sbar, the intended*[name='foo%sbar']matches elementb; Selenium's*[name='foofoo\%sbarbar']matches a different elementab. The wrong-target case produces no error anywhere.Expected vs Actual
By.name("50%off")builds a fallback selector that matches an element withname="50%off";%in a name is a literal character.UnknownFormat/IllegalFormatConversionException, or (for names containing sequences like%sthat 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:ByNamepre-formats the value into the pattern and passes the result asformatString.java/src/org/openqa/selenium/By.java:427:PreW3CLocatorre-runsString.format(formatString, cssEscape(value))over it.Suggested fix
Make
ByNamebehave likeById/ByClassName: pass a plain literal format string and letPreW3CLocatordo the single format pass over the escaped value.The existing
cssEscape(value)inPreW3CLocatorthen handles the quoting ('is already in theCSS_ESCAPEcharacter class, so the manualname.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):
4.46.0trunk)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.