Commit Graph

4 Commits

Author SHA1 Message Date
FilipBl4gojevic
ea3a23e5f9 fix(signer): match own addresses after trimming, not before
knownAddresses validated the trimmed value but stored the original, so an
own address configured with an incidental leading or trailing space passed
the fail-closed gate yet could never match its own outputs - every change
output was then counted as outflow and normal transactions were refused.

Also pins prefix-match, trim-before-check, and case-sensitivity, none of
which any test previously required.
2026-07-28 13:18:58 +02:00
FilipBl4gojevic
97987427ce fix(signer): close two review-found gaps in ownAddresses validation
Fix 1 (Important, live bypass): the blank-address fix from the prior commit
used .trim().length > 0, which only strips the ECMAScript WhiteSpace set
(NBSP U+00A0, BOM U+FEFF, etc). U+200B ZERO WIDTH SPACE and U+2060 WORD
JOINER are Unicode category Cf (format), not Zs, so trim() leaves them
untouched - ownAddresses: ['\u200B'] passed the filter, bypassed the
fail-closed guard, and reproduced allowed:true against the shipped code
(verified directly, not just against a mutant). Replaced the blank-only
filter with a positive check: an entry only counts as a known own address if
it starts with 'addr' after trimming. A denylist of invisible codepoints is
unwinnable; this rules out anything built from them without enumerating any.
Applies at both call sites (evaluate's guard and computeOutflow's Set) via
the shared knownAddresses() helper, as it already did.

Fix 2 (Important, surviving mutant): added a regression test that exercises
computeOutflow's own Set-construction filtering directly, independent of
evaluate's separate guard - a blank/invisible output address paired with a
matching blank/invisible ownAddresses entry must still count as outflow, not
be matched as change. Nothing previously exercised this combination: every
prior blank-entry test either went through evaluate (whose guard would
refuse first) or paired a blank ownAddresses entry with a real output
address. Confirmed this test now fails if computeOutflow's Set reverts to
unfiltered while evaluate's guard stays filtered (re-verified in this
session; see the report addendum for output).

Fix 3: added a doc comment on evaluate() stating plainly that it bounds
lovelace only - assetMovements is reported for visibility and never gates
allowed. No interface change; this is documentation of the existing,
deliberate v1 scope boundary already flagged in the task report, not new
behavior.

Re-ran the full mutation set after all three fixes: inverted classification,
removed fail-closed guard entirely, guard reverted to raw
ownAddresses.length === 0, and computeOutflow's Set left unfiltered are all
caught. Full before/after evidence in the report addendum.
2026-07-28 12:57:45 +02:00
FilipBl4gojevic
9bbd26b04c fix(signer): treat blank-only ownAddresses as no known own address
Adversarial pass on the spend policy found that ownAddresses.length === 0
does not catch a non-empty array containing only blank entries - a realistic
outcome of ''.split(',') on an empty config/env value, which yields [''] not
[]. A blank entry never matches a real output address, so it degrades to
"every output looks foreign" rather than permitting anything, but it bypassed
the explicit fail-closed path and its message, and if the resulting
over-counted outflow still happened to land under the configured limits the
transaction would be signed without any real own address ever having been
known - exactly the condition that check exists to refuse.

knownAddresses() filters blank entries before both the Set used for
classification and the fail-closed emptiness check, so an all-blank list now
refuses the same way a literally empty one does. A blank entry alongside a
real address is unaffected (regression-tested).

Also adds the rest of the adversarial pass: exact-limit boundaries one
lovelace either side, many-small-outputs summation, aggregation correctness
(duplicate own addresses, repeated foreign addresses, multi-asset and
multi-output asset movements), check-ordering pins, zero-limit and
already-over-budget edge cases, and a pinned note that SpendLimits governs
lovelace only - native asset movements are tracked but not limited.

Every core comparison was verified to actually fail on injection (inverted
own/foreign check, dropped fee, dropped daily history, off-by-one on both
limit comparisons) before being reverted; see the task report.
2026-07-28 12:30:14 +02:00
FilipBl4gojevic
65a3c00994 feat(signer): spend policy over true net outflow
Net outflow is every output that does not return to us, plus the fee - more
accurate than summing declared amounts, and computable without chain access.
Refuses when no own address is known, since change cannot then be identified.
2026-07-28 12:23:12 +02:00