diff --git a/internal/driver/chrome/translate.go b/internal/driver/chrome/translate.go index 7b0295b..a1ca3be 100644 --- a/internal/driver/chrome/translate.go +++ b/internal/driver/chrome/translate.go @@ -3,10 +3,16 @@ package chrome import ( "errors" "fmt" + "regexp" "strings" "unicode/utf8" ) +// attrNamePattern matches HTML attribute names that are safe to drop into a +// CSS attribute selector without escaping. This avoids selectors like +// `foo]:has(*),body[x="..."]` that would escape the intended match. +var attrNamePattern = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_-]*$`) + // TranslateStringSelector converts a legacy string selector ("id:foo", // "descPrefix:bar") into a CSS selector or XPath expression usable from the // chrome driver's TapSelector fallback path. The boolean return is true when @@ -41,6 +47,9 @@ func TranslateStringSelector(selector string) (string, bool, error) { case "placeholder", "placeholderValue", "hintText": return `[placeholder="` + cssEscape(value) + `"]`, false, nil default: + if !attrNamePattern.MatchString(kind) { + return "", false, fmt.Errorf("unsafe selector prefix %q", kind) + } return `[` + kind + `="` + cssEscape(value) + `"]`, false, nil } } diff --git a/internal/driver/chrome/translate_test.go b/internal/driver/chrome/translate_test.go index ad05d10..797d14d 100644 --- a/internal/driver/chrome/translate_test.go +++ b/internal/driver/chrome/translate_test.go @@ -64,6 +64,21 @@ func TestTranslateStringSelector_RejectsMissingPrefix(t *testing.T) { } } +func TestTranslateStringSelector_RejectsUnsafePrefix(t *testing.T) { + cases := []string{ + `foo]:has(*),body[x:value`, + `x y:value`, + `x"y:value`, + `*:value`, + `(:value`, + } + for _, selector := range cases { + if _, _, err := TranslateStringSelector(selector); err == nil { + t.Errorf("%q: expected error for unsafe prefix", selector) + } + } +} + func TestCSSEscape_ControlCharactersAndNUL(t *testing.T) { cases := []struct { name string