HarperFast / HarperFast/harper
Prototype-chain key/attribute names: one __proto__ PK poisons SQL/index reads table-wide; proto-named attributes falsely rejected at insert
- Dominant language
- JavaScript
- Stars
- 89
- Forks
- 10
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 200
Description
## Summary
A class of bugs from **unguarded `for-in` / bracket property-lookup over plain objects that inherit from `Object.prototype`**. When a key or attribute name equals an inherited member (`__proto__`, `constructor`, `prototype`, `toString`, `hasOwnProperty`, `valueOf`, `isPrototypeOf`, `propertyIsEnumerable`, `toLocaleString`, `__defineGetter__`/`__defineSetter__`/`__lookupGetter__`/`__lookupSetter__`), the lookup resolves the inherited member instead of "absent." Prototype pollution itself is **safe** (no `Object.prototype` mutation; instance survives) — the impact is correctness/availability.
### Symptom A — prototype-name PK VALUES break SQL/read paths
Rows keyed by a prototype name round-trip byte-exact via `search_by_id` and REST GET, but:
- `SELECT id FROM data.T` returns only 1 of 7 such rows — **silent omission**.
- SQL aggregates 500 (`SQLSearch._convertColumnsToIndexes`, `__mergedAttributes` undefined).
- `DELETE FROM` throws `TypeError: resolver is not a function` at `Table.js updateIndices` (for-in over `indices` resolves inherited `propertyResolvers['__defineGetter__']`).
- Minor: REST read-back mangles a `__proto__` key → `__proto_`.
### Symptom B — prototype-name ATTRIBUTES falsely rejected at insert
The ops `insert` validator (`validation/insertValidator.ts`) uses `INVALID_ATTRIBUTE_NAMES = { undefined, null }` and checks `INVALID_ATTRIBUTE_NAMES[attr] !== undefined`, which resolves **inherited** prototype members → all 11 prototype-member names are falsely rejected with `400 "Invalid attribute name"`. Intended denylist is 2 names; actual is 2 + every prototype member. Such columns are permanently unwritable via the ops API (fail-stop). **Plausible real trigger:** schema-less / heterogeneous ingest of third-party JSON carrying a `constructor`/`valueOf`/`toString` field.
### Symptom C — table-wide search poisoning (severity escalator)
Inserting **one** row with a `__proto__` primary key poisons the secondary-index / predicate read path for the **entire table**:
- SQL `WHERE` queries 500 for **all** rows.
- ops `search_by_value` returns `[{"id":null}]` (one garbage row) instead of the full set — a **silent, table-wide wrong result**.
- Full-scan `SELECT *`, REST get-by-id, and REST collection stay correct (so it's specifically the indexed/predicate path).
This makes the bug a table-wide denial-of-search + silent-wrong-result, **attacker-injectable** if external input can become a primary key (user-chosen IDs, ingested external keys). Fix locus: key→attribute mapping `dataLayer/SQLSearch.ts:1311` + the shared index-read code.
## Suggested fix
Replace the offending plain-object maps/loops with `Set` / `Object.create(null)` / `Map`, or guard every lookup with `Object.hasOwn`. Known sites: `insertValidator` (`INVALID_ATTRIBUTE_NAMES`), the SQL attribute-merge/index path (`SQLSearch`), `Table.updateIndices` (`indices`/`propertyResolvers`). Low-risk, mechanical.
## Repro
- Symptom A: `integrationTests/qa-scratch/reserved-names.test.ts`
- Symptom B: `integrationTests/qa-scratch/proto-field-names.test.ts`
- Symptom C: `integrationTests/qa-scratch/sql-rest-parity.test.ts`
---
_Found via the exploratory QA campaign (qa-explorer), scenarios QA-044 / QA-049 / QA-050. Harper `001bf7b9c` (v5.1.0, main)._
Contributor guide
Assessment
This issue has not been assessed yet.