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:
parent
134f052457
commit
9a0253e724
28 changed files with 1729 additions and 3 deletions
|
|
@ -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×)
|
||||
|
|
@ -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×)
|
||||
|
|
@ -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×)
|
||||
|
|
@ -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×)
|
||||
Loading…
Add table
Add a link
Reference in a new issue