browser-automation: 4 CWE-407 patches (selenium x2, playwright, webdriverio)

selenium-0001: SessionCapabilitiesMutator list.contains O(NxM) -> LinkedHashSet
  O(N+M). Grid Node session mutation hot path. Bench: 192x at N=M=1000.

selenium-0002: ChromiumOptions merge helpers consolidate four list.contains
  loops behind addArgumentsUnique/addEncodedExtensionsUnique. Bench: 254x
  at N=M=1000.

playwright-0001: roleUtils validRoles / allowsNameFromContent Array.includes
  on 20-70 element constant arrays per element. Converted to Set<string>
  at module load. Bench: 11x at N=10000 elements.

webdriverio-0001: xpath-conditions extractOrConditions orMatches.find +
  values.includes per regex match -> Map<attr, Set<values>>. Bench: 6x at
  K=V=60 in the 'mobileSelectorPerformanceOptimizer'.

Each defect ships: ticket, patch with complexity-gate header, Python
benchmark + correctness test, Makefile, outreach brief. All 16 tests
pass. UNDF IDs: 1276 (playwright), 1277 (selenium-0001), 1288
(selenium-0002), 1289 (webdriverio).
This commit is contained in:
russell@unturf.com 2026-04-22 18:14:39 -04:00
parent 134f052457
commit 9a0253e724
28 changed files with 1729 additions and 3 deletions

View file

@ -0,0 +1,91 @@
# playwright-0001: roleUtils — O(N×k) ARIA role validation in per-element snapshot
**Target:** microsoft/playwright
**Severity:** MEDIUM-HIGH
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**MOAD:** MOAD-0001 (A Sedimentary Defect)
**File:** `packages/injected/src/roleUtils.ts:262-268, 499-500, 51-52, 262-268`
**Language:** TypeScript
**Status:** open
## Description
Playwright injects `roleUtils.ts` into every target page to compute accessible
names and ARIA roles. Several frequently-called helpers use `Array.includes` on
constant arrays holding 70+ role strings. When Playwright snapshots a page's
accessibility tree, `getExplicitAriaRole`, `allowsNameFromContent`, and
`hasGlobalAriaAttribute` run once per element — thousands of times on modern
pages.
Each `Array.includes` call is O(k) where k is the array length. Inside a DOM
traversal of N elements, total cost is O(N×k). On a page with 5000 elements
and the 70-entry `validRoles` array, that's 350,000 string comparisons per
snapshot. Converting the arrays to `Set<string>` drops lookup to O(1),
producing O(N+k).
## Root Cause
```typescript
// roleUtils.ts:262-268
const validRoles: AriaRole[] = ['alert', 'alertdialog', 'application', ...70 items];
function getExplicitAriaRole(element: Element): AriaRole | null {
const roles = (element.getAttribute('role') || '').split(' ').map(...);
return roles.find(role => validRoles.includes(role as any)) as AriaRole || null;
}
// roleUtils.ts:499-500 (inside allowsNameFromContent — called per element)
const alwaysAllowsNameFromContent = [
'button', 'cell', 'checkbox', 'columnheader', ...20 items
].includes(role);
const descendantAllowsNameFromContent = targetDescendant && [
'', 'caption', 'code', ...30 items
].includes(role);
// roleUtils.ts:51-52 (kGlobalAriaAttributes prohibited-list scan inside
// hasGlobalAriaAttribute, called per element):
!prohibited?.includes(forRole || '')
```
Each `Array.includes` is O(k). Called per-element across thousands of
elements, cost compounds to O(N×k). The arrays are constant and could be
built once as Sets at module load.
## Fix
Convert hot-path constant arrays to `Set<string>` at module scope, use
`set.has(x)` for O(1) lookup:
```typescript
const validRolesSet = new Set<string>(validRoles);
function getExplicitAriaRole(element: Element): AriaRole | null {
const roles = (element.getAttribute('role') || '').split(' ').map(...);
return roles.find(role => validRolesSet.has(role)) as AriaRole || null;
}
const alwaysAllowsNameFromContentSet = new Set([
'button', 'cell', 'checkbox', ...
]);
const descendantAllowsNameFromContentSet = new Set([...]);
function allowsNameFromContent(role: string, targetDescendant: boolean) {
return alwaysAllowsNameFromContentSet.has(role) ||
(targetDescendant && descendantAllowsNameFromContentSet.has(role));
}
```
Set construction runs once at module load. Per-element lookups drop to O(1).
## Severity Note
Runs on every accessibility snapshot, every `getByRole` locator, every ARIA
tree traversal. Per-element cost is microseconds, but pages with 5000+
elements compound quickly. Large enterprise apps (dashboards, CRMs, grid
layouts) commonly exceed this element count. Impact amplifies under
`@playwright/test` parallel runs with `toHaveAccessibleName` assertions.
## Complexity Gate
- N=5000 elements, 70-element role arrays: fixed must complete in <10ms
- k-scaling 5×: time ratio must be <17.5× (O(k) ≈5×, not O(k²) ≈25×)

View file

@ -0,0 +1,96 @@
# selenium-0001: SessionCapabilitiesMutator — O(N×M) args/extensions dedup per session
**Target:** SeleniumHQ/selenium
**Severity:** MEDIUM-HIGH
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**MOAD:** MOAD-0001 (A Sedimentary Defect)
**File:** `java/src/org/openqa/selenium/grid/node/config/SessionCapabilitiesMutator.java:136-141, 154-159, 194-199`
**Language:** Java
**Status:** open
## Description
Selenium Grid Node runs `SessionCapabilitiesMutator` on every incoming session
creation request. It merges the node's slot stereotype capabilities with the
client-requested capabilities. For the Chromium and Firefox merge paths, the
`args` and `extensions` arrays are deduplicated via `List.contains` inside a
`forEach` loop, giving O(N×M) where N is the client args/extensions count and
M is the stereotype's existing arg/extension count.
A Grid hub handling N parallel sessions per second pays this cost N times per
second. On CI farms running large Chrome arg lists (20–50 flags plus an
extension profile), the per-session cost compounds.
## Root Cause
```java
// SessionCapabilitiesMutator.java:120-142 mergeChromiumOptions
for (Map.Entry<String, Object> entry : capsOptions.entrySet()) {
String name = entry.getKey();
Object value = entry.getValue();
if (name.equals("args")) {
List<String> arguments = new ArrayList<>((List<String>) value);
List<String> stereotypeArguments =
new ArrayList<>(
(List<String>) (stereotypeOptions.getOrDefault(("args"), new ArrayList<>())));
arguments.forEach(
arg -> {
if (!stereotypeArguments.contains(arg)) { // O(M) scan per arg
stereotypeArguments.add(arg);
}
});
toReturn.put("args", stereotypeArguments);
}
if (name.equals("extensions")) {
// same pattern for extensions (lines 154-159)
...
extensionList.forEach(
extension -> {
if (!stereotypeExtensions.contains(extension)) { // O(M) scan per extension
stereotypeExtensions.add(extension);
}
});
...
}
}
// mergeFirefoxOptions (lines 194-199): identical pattern for args
```
`ArrayList.contains()` is O(M). Called inside `forEach(List)` of size N, total
cost is O(N×M) per session per merged list (args and extensions both).
## Fix
Build a `LinkedHashSet` once from the stereotype list (preserves order, O(1)
lookup), then iterate the client list and add only new items. Writes the final
list back from the set via `new ArrayList<>(set)`.
```java
Set<String> stereotypeArgSet = new LinkedHashSet<>(stereotypeArguments);
for (String arg : arguments) {
if (stereotypeArgSet.add(arg)) {
stereotypeArguments.add(arg);
}
}
```
Cost becomes O(N+M): one Set build + N Set lookups with amortized O(1) `add`.
Semantics preserved: `LinkedHashSet` preserves insertion order matching the
original `ArrayList.contains`-based dedup, and `Set.add` returns true only on
first insertion, matching the original if-not-contains-then-add guard.
## Severity Note
Affects every session-creation request routed through a Grid Node. Impact grows
linearly with session throughput and with the size of Chrome flag lists.
Typical production CI farms handle 10–100 sessions/sec with 20–50 args per
session, giving a measurable but non-critical wall-clock cost. Greatest benefit
accrues to large enterprises running Selenium Grid at scale.
## Complexity Gate
- N=M=20: fixed must complete in <1ms per session
- k-scaling 5×: time ratio must be <17.5× (O(k) ≈5×, not O(k²) ≈25×)

View file

@ -0,0 +1,77 @@
# selenium-0002: ChromiumOptions — O(N×M) args/extensions dedup in merge paths
**Target:** SeleniumHQ/selenium
**Severity:** MEDIUM
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**MOAD:** MOAD-0001 (A Sedimentary Defect)
**File:** `java/src/org/openqa/selenium/chromium/ChromiumOptions.java:283-303, 317-323, 345-361`
**Language:** Java
**Status:** open
## Description
`ChromiumOptions` is the client-side builder every Selenium Java client uses
for Chrome, Edge, and Chromium-based browsers. Its `mergeInPlace` and
`mergeInOptionsFromCaps` methods deduplicate incoming args and extensions
against the existing lists via `List.contains` inside `forEach`, producing
O(N×M) per merge.
Clients often call `merge` inside test setup for every test in a suite,
particularly under parallelized frameworks that build capabilities per worker
thread. N args of 20–50 flags plus M existing args accumulate over repeated
merges.
## Root Cause
```java
// ChromiumOptions.java:281-289 mergeInPlace, args handling
if (name.equals("args") && capabilities.getCapability(name) != null) {
List<String> arguments = capabilities.required("args");
arguments.forEach(
arg -> {
if (!args.contains(arg)) { // O(M)
addArguments(arg);
}
});
}
// Lines 291-303: same pattern for extensions inside mergeInPlace
// Lines 317-321: nested inner loop doing the same check on options.args
// Lines 345-350 and 352-361: same pattern in mergeInOptionsFromCaps
```
Four separate loops carry the identical `!list.contains(x)` inside forEach
pattern. Each is O(N×M) per call.
## Fix
Introduce helper methods `addArgumentsUnique(List<String>)` and
`addEncodedExtensionsUnique(List<String>)` that pre-build a `HashSet` from the
current list, then iterate the incoming list with O(1) lookups. All four call
sites switch to these helpers, eliminating the quadratic pattern.
```java
private void addArgumentsUnique(Collection<String> toAdd) {
Set<String> seen = new HashSet<>(args);
for (String arg : toAdd) {
if (seen.add(arg)) {
args.add(arg);
}
}
}
```
Cost becomes O(N+M) per call, one HashSet build amortized across all lookups.
## Severity Note
Client-side code path. Overhead paid once per `merge` call. Parallelized test
suites with per-worker option builders accumulate merge cost; typical impact is
measured in micro- to low-milliseconds per merge with args counts of 20–50.
Lower priority than selenium-0001 (server-side hot path) but cleanup on the
same pattern.
## Complexity Gate
- N=M=50: fixed must complete in <1ms per merge
- k-scaling 5×: time ratio must be <17.5× (O(k) ≈5×, not O(k²) ≈25×)

View file

@ -0,0 +1,89 @@
# webdriverio-0001: xpath-conditions extractOrConditions — O(N²) in selector optimizer
**Target:** webdriverio/webdriverio
**Severity:** MEDIUM
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**MOAD:** MOAD-0001 (A Sedimentary Defect)
**File:** `packages/wdio-appium-service/src/mobileSelectorPerformanceOptimizer/utils/xpath-conditions.ts:43-81`
**Language:** TypeScript
**Status:** open
## Description
`@wdio/appium-service` ships a "mobileSelectorPerformanceOptimizer" that
converts XPath selectors to native NSPredicate / iOS Class Chain syntax for
faster element location on mobile. Ironically the optimizer itself contains an
O(N²) pattern: `extractOrConditions` builds an `orMatches` array, then for
every regex match calls `orMatches.find` to locate an existing entry, followed
by two `.values.includes` checks to dedup values.
With K distinct attributes and V values per attribute, total cost is O(K×V²)
for deduplication, plus O(K×V) for the `find` scans. On long disjunction
chains (e.g. `@class="a" or @class="b" or @class="c"...`) this compounds.
## Root Cause
```typescript
// xpath-conditions.ts:50-67
while ((orMatch = orPattern.exec(content)) !== null) {
if (orMatch[1] === orMatch[3]) {
const existing = orMatches.find(m => m.attr === orMatch![1]); // O(K) per match
if (existing) {
if (!existing.values.includes(orMatch[2])) { // O(V)
existing.values.push(orMatch[2]);
}
if (!existing.values.includes(orMatch[4])) { // O(V)
existing.values.push(orMatch[4]);
}
} else {
orMatches.push({ attr: orMatch[1], values: [orMatch[2], orMatch[4]] });
}
}
}
```
`Array.find` is O(K) where K = number of distinct attributes seen so far.
Inner `Array.includes` on `existing.values` is O(V) where V = values per
attribute. Total: O(M×K + M×V) where M = total regex matches.
## Fix
Replace `orMatches` array-of-objects with a `Map<string, Set<string>>`. Map
lookup is O(1), Set membership test is O(1). Final materialization walks
entries in insertion order.
```typescript
const orMatches = new Map<string, Set<string>>();
while ((orMatch = orPattern.exec(content)) !== null) {
if (orMatch[1] === orMatch[3]) {
let values = orMatches.get(orMatch[1]);
if (!values) {
values = new Set<string>();
orMatches.set(orMatch[1], values);
}
values.add(orMatch[2]);
values.add(orMatch[4]);
}
}
for (const [attr, valueSet] of orMatches) {
for (const value of valueSet) {
conditions.push({ attribute: attr, operator: '=', value, logicalOp: 'OR' });
}
}
```
All operations become amortized O(1). Total cost drops to O(M).
## Severity Note
Called per selector conversion during mobile test setup. Most selectors have
few OR conditions, but long chains in data-driven tests (e.g. matching any of
N product IDs) hit this. Low-to-medium priority — file lives in a file titled
"PerformanceOptimizer" which should set the bar for its own implementation.
## Complexity Gate
- K=20 attrs × V=20 values: fixed must complete in <1ms
- k-scaling 5×: time ratio must be <17.5× (O(k) ≈5×, not O(k²) ≈25×)