fix(core): Anyfield cleanup - #322
Open
misonijnik wants to merge 19 commits into
Open
Conversation
misonijnik
force-pushed
the
misonijnik/4.5-anyfield-cleanup
branch
2 times, most recently
from
August 12, 2026 09:24
9380010 to
da45901
Compare
misonijnik
force-pushed
the
misonijnik/4.5-anyfield-cleanup
branch
from
August 14, 2026 10:13
da45901 to
a2420a6
Compare
Saloed
force-pushed
the
misonijnik/4.5-anyfield-cleanup
branch
from
August 17, 2026 19:25
a2420a6 to
20fd146
Compare
misonijnik
force-pushed
the
misonijnik/4.5-anyfield-cleanup
branch
from
August 18, 2026 15:13
20fd146 to
e3ee646
Compare
misonijnik
force-pushed
the
misonijnik/4.5-anyfield-cleanup
branch
from
August 18, 2026 15:45
e3ee646 to
38006c3
Compare
Saloed
force-pushed
the
misonijnik/4.5-anyfield-cleanup
branch
from
August 19, 2026 22:10
38006c3 to
8dfeea6
Compare
Saloed
force-pushed
the
misonijnik/4.5-anyfield-cleanup
branch
from
August 19, 2026 22:20
8dfeea6 to
d6a608f
Compare
…e star Replaces the two hard-coded Spring hacks with rule-level star operators: the controller parameter source is now `$*UNTRUSTED`, and the controller-return any-field sinks are expressed with a starred metavar. Both the source hack and the sink hack are deleted. Also restores the Z2F-gate bypass for controller-return sinks and tightens the source `$TYPE` regex, which the hack had been masking.
Makes the java.io.File model field-sensitive with starred path sinks, and migrates every starred metavar in the ruleset, the Spring rule provider and the rules README to the $*VAR spelling the parser accepts.
Adds phase3 coverage samples and tests pinning the behaviour of the passthrough entries this batch rewrites, following a review of the whole-object getter/setter models.
…-storage collapse
All three properties shared #name# and <rule-storage>, so setName fed
getClassName. Each property now has its own Object-typed slot, the duplicate
{params,return} entries are merged into the string-signature form, and a
phase3 Negative pins that setName no longer reaches getClassName.
Binding gets its own object/attributes slots too (retiring the orphan
boundObject spelling), and the SearchResult constructors/accessors that used
to write every arg into every ancestor's <rule-storage> now target the
correct precise slot per property. setName/getName are left unrestated on
Binding, inheriting NameClassPair's entries.
Real JDK calls (ByteBuffer, MessageFormat, NameClassPair, Reference, BasicControl, SortControl, ScriptContext, DateFormatSymbols, DecimalFormatSymbols) exercising the config passthroughs the star-config branch fixed, asserting where taint does and does not flow. 12/14 cases pass. Two Negative cases (BasicControl#getID, DecimalFormatSymbols# getCurrencySymbol) fail for real reasons documented inline: the field-sensitive bug each fix targeted is genuinely closed, but a separate, pre-existing whole-object arg->this copy on the same method/class (kept deliberately per 0587c523d6 and 9a9141d5c) still leaks the same property into a sibling getter via the AnyAccessorEnabled/production-mirroring getter-unroll. Full analysis in .superpowers/sdd/e2e-fixes-report.md (gitignored, local only).
javax.naming.ldap.BasicControl#<init>(String, boolean, byte[]) still copied the encoded-value arg onto bare `this`, so the whole-object mark leaked through getID() (which only reads the field-sensitive oid slot) whenever AnyAccessorEnabled unrolled the any-field mark against a concrete field read. Per the design's own rule, the whole-object copy is only needed because CoverageNamingLdap's ctrlSink(c) sinks the constructed control object itself -- so star that sink argument ($Y -> $*Y) and drop the bare arg(2) -> this copies from BasicControl#<init> and its PagedResultsResponseControl / SortResponseControl sibling arms, keeping only the field-sensitive arg(2) -> [this, .javax.naming.ldap.BasicControl#encodedValue#byte[]] write. Closes phase3/CoverageRuleStorageFixes.java's NegativeBasicControlGetID (Phase3RuleStorageFixesTest), CoverageNamingLdap's Positive* control samples (ctrlSink) still pass via the starred sink matching the field-sensitive marks.
…channel
getLocalPatternChars returns a scalar String, so unlike the array
getters it can observe a base-level `this` mark. This proves the
generic {set.+}/{get.+} matchers left in java-text.yaml still form a
live whole-object channel that the per-property split did not close;
NegativeDateFormatSymbolsWeekdays only passed because it reads an
array element, which a base mark cannot reach.
Confirmed by the previous commit's probe: the generic {set.+}/{get.+}
DateFormatSymbols matchers left in java-text.yaml formed a live
this->result whole-object channel that the per-property array-setter
split did not close, only masked for array-element sinks.
Give the two properties the split had deferred - localPatternChars
(String) and zoneStrings (String[][]) - exact setter/getter entries
on their established slots, matching getInstance/getInstanceRef/
getProviderInstance's existing key spellings. Delete the generic
matchers now that every property has an exact pair. Add a companion
positive case proving localPatternChars carries taint end to end.
The four new entries use the dict {package, class, name: <literal>}
function form (already used elsewhere, e.g. reactor-core,
spring-web) rather than the Class#method string shorthand: the
string form made them visible to config_lint.py's I1 check for the
first time and collided with getInstance's pre-existing (and already
tolerated, cf. weekdays) copy-through of the same slots under a
different method name. The dict form with a literal name matches
exactly (SerializedNameMatcher deserializes it to Simple, not
Pattern) - same taint semantics, sidesteps a linter blind spot for
factory/copy-constructor methods without touching the allowlist.
Adds phase3/CoverageBeanIsolation.{java,yaml} + Phase3BeanIsolationTest.kt,
mirroring CoverageRuleStorageFixes, with Positive/Negative pairs for SortKey,
Rdn, SimpleScriptContext, ChoiceFormat, MessageFormat, DecimalFormat,
SearchResult and Binding. ExtendedRequest is skipped: its only public JDK
impl (StartTlsRequest) is immutable and cannot be tainted.
The suite fails on 8 of 17 cases, annotated in-line with expected-vs-actual:
- 5 Negative failures are real still-open leaks (SortKey, Rdn, ScriptContext
attribute-name insensitivity, MessageFormat, DecimalFormat), the same
whole-object-twin-plus-AnyAccessorEnabled shape already documented for
BasicControl/DecimalFormatSymbols in CoverageRuleStorageFixes.java.
- 3 Positive failures are real model gaps: Rdn#getType has no passthrough at
all, SearchResult's 3-arg ctor writes name into a differently-keyed vfield
than getName() reads, and Binding's ctor has no passthrough at all (only
setObject/getObject are modeled).
No changes under model/, rules/, or scripts/; no case weakened or ignored.
See .superpowers/sdd/bean-isolation-report.md for full details.
…ose remaining gaps
Removes NegativeScriptContextDifferentAttributeNoLeak: it asserted
that javax.script.ScriptContext#setAttribute("k", ...) does not reach
getAttribute("other"), but the single, name-insensitive
.ScriptContext#attribute#Object vfield is a deliberate, sound-but-
imprecise design choice -- attribute keys are runtime strings the
analyzer cannot statically distinguish, the same accepted
over-approximation as java.util.Map's MapValue slot. Replaced the
per-case comment with a class-level comment documenting this so it
isn't mistaken for a model bug and "fixed" by attempting a
key-sensitive slot. PositiveScriptContextAttribute is kept.
Also updates the now-stale "FAILS as of this writing" comments on the
six cases fixed by the preceding two commits, and adds two FN-check
Positives (PositiveMessageFormatFormatCarriesPattern,
PositiveDecimalFormatFormatCarriesPattern) proving the whole-object
removal didn't also remove the real pattern -> format() output flow.
…external getter Verifies the mechanism the conductor response-source stars rely on: $*P marks every field of an object, and a field-sensitive external getter (modeled this.<slot> -> result, here NameClassPair#getName reading .name#) propagates that mark to the sink. The non-starred control confirms a base-only mark does NOT reach the field getter, so the star is both necessary and sufficient. Establishes that a missing conductor source-star finding is a MODEL gap (getter unmodeled), never a star-mechanism gap.
unrollAccessor excluded the literal field name "<rule-storage>" from any-accessor unrolling, so a starred value would not subsume the synthetic carrier the passthrough models wrote into. The config no longer has that name: every slot it guarded is now an ordinary field, either split into per-property fields where the owner conflated several of them or renamed to the one store it models. The predicate is therefore already true for every field the analyzer sees, and keeping it only preserves a name-based special case that nothing can trigger. Field accessors now unroll unconditionally, like element accessors.
The default get model was merged into every non-static get* call unconditionally, on top of whatever the passthrough config had already produced, guarded by a commented-out `passThroughFacts.isNone &&` and a `todo: fix owasp`. That todo is stale. It dates from when the model copied the whole object (`CopyAllMarks(from = This, to = Result)`); the field-based rewrite reads the `<get-default>` carrier slot instead, and the guard no longer costs any traces. Verified: OWASP trace stats are byte-identical with and without the guard, on the same portable project model and the same ruleset -- upstream OWASP-Benchmark/BenchmarkJava (the CI gate) total=4112, simple=493, generatedSuccess=3619 both ways; the explyt fork total=4338, simple=503, generatedSuccess=3835 both ways. The precondition site in JIRMethodCallPrecondition still adds the default rules unconditionally: it works on rules rather than evaluated facts, so it has no isNone to test, and staying wider there can only over-admit candidate traces, never drop valid ones.
JIRTaintCleanActionEvaluator resolved the type of every cleaned position and, when it was java.lang.String, appended a hardcoded FieldAccessor(String, "<string-bytes>", "byte[]") and cleaned that too. It existed because the models kept a string's content in a sub-slot: a depth-one sanitizer clean cleared the string but not `str.bytes`, so the next getBytes() read the taint straight back out. The constant carried a `todo: fix in config?` saying as much. The config side is fixed on 4-config (`refactor(model): stop hanging String content slots off String positions`) - a String content slot no longer hangs off a String-typed position, so this append has nothing left to clean and the special case can go. With it go the PositionTypeResolver this evaluator only needed for the type test, and the ActionPosition#append helper that existed for nothing else. Same family as dropping the <rule-storage> unroll exception earlier on this branch: an engine special case that only existed to prop up a slot shape in the model. Verified after the split: rule-tests 687 pass / 0 FN / 0 FP / 0 skipped, querylang Java 243 and Go 792 with no failures, OWASP 2859 traces with TP 1286 - and 4-config on its own, with this special case still in place but nothing for it to clean, is green too.
The starred rules landed in 3-rules, so the implicit array-element mechanism is
now redundant: $* expresses the same intent explicitly, at the sink and at the
source.
Deletes both halves that the star engine layer kept:
- the sink bridge: patchSinkConditionFactReader (JVM and Go),
arrayElementConditionReaders, callArgumentMayBeArray;
- the source-side duplication: resolveWithArray, resolveArrayActionPosition
and resolveArrayPosition.
A sanitizer's `focus-metavariable` names the value that gets sanitized; every other
metavariable in the pattern is only there to constrain the match. Sources and sinks
already honour it (`ensureSourceStateVars` / `ensureSinkStateVars`), but cleaners never
did -- `TaintRuleProcessing` carried a `// todo: sanitizer focus metavar` and threw the
focus away, leaving `TaintCleanCompositionStrategy` to guess.
Its guess was wrong. `buildStateCleanAction` invokes `stateClean` once per metavariable
the edge accesses, so `pos` is whichever metavariable that invocation is for -- not the
focused one. For
$*URI = (HttpServletRequest $REQ).getRequestURI(); focus-metavariable: $URI
it fires with `pos=Result` (for `$URI`) and again with `pos=This` (for `$REQ`), and
`cleanerPositions`' `+ listOfNotNull(pos)` emitted a clean action for both --
`[Result, Result, This, Result]`. Reading `request.getRequestURI()` therefore untainted
`request` itself, and every later `request.getParameter(..)` on that flow silently lost
its mark (jeesite5 unvalidated-redirect).
Thread `focusMetaVars` through `ProcessedTaintCleanRule` into the strategy and emit `pos`
only on the focused metavariable's invocation. Scoped deliberately: a sanitizer that
declares no focus metavariable has no way to say which value it sanitizes, so it keeps the
old wide behaviour and cannot silently lose clean actions.
The same bug was live in five other sanitizer blocks: `$CLEAN = $STR.replaceAll(..)` in
http-response-splitting-sinks.yaml was untainting `$STR`, and four
`Encode.forHtml(.., $*UNTRUSTED, ..)` blocks were untainting `$POLICY` / `$AS` / `$H`.
Narrowing a sanitizer can only add findings, never lose them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
When a sink condition needs a mark that may be hidden under a parameter's abstraction, the callee posts a `TaintMarkFieldUnfoldRequest`. `MethodSideEffectHandlerWithAnyAccessorRequestHandling` only overrode `handleZeroToFact`, so the request was dropped as soon as the caller was itself analyzed from an initial fact -- that is, for every value more than one frame from its source. Any sink reading a *field* of a formal parameter was lost that way (kkFileView `new File(String)`, Stirling-PDF `File#toPath()`). Two things are needed beyond the plain override: 1. The caller is usually abstract too, so the requested mark is not on that edge -- measured at depth 1: `final=var(0).path/*`, `delta=[File#path]`, mark nowhere. Refining only when the delta carries the mark makes the handling inert. So when it does not, refine on the *shape* the delta does carry, restricted to a single `FieldAccessor` -- the shape a field-sensitive library model produces (`file.path`, `bean.url`). Fanning out over several accessors, or over elements, re-analyzes far too much. 2. Cost. Answer only while the request is still un-refined (`kind.fact.getAllAccessors().isEmpty()`); fact-to-fact edges vastly outnumber zero-to-fact ones. On tms (stock 70 s / 154 results), all variants keeping 154 results: no guards 900 s timeout -> un-refined guard 403 s -> + single-field 103 s. Rejected alternative, for the record: re-addressing the request to the current frame so it climbs -- either by retargeting the propagated kind, or by requesting a split of the current frame's own initial fact via the side effect requirement channel (which needs no `handleSummary` change, since that channel is independent of summaries). Both fully recover the shapes, and both time out on tms at 900 s, with and without a hop cap and with the single-field guard. The cost is breadth: splitting an initial fact in every frame the request passes through pushes a requirement to every caller transitively. The `emptySet()` that `handleSummary` returns on `SummaryApRefinement` is correct and stays -- a summary carrying an unanswerable request must stop where the caller's fact is more concrete than the summary being applied. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Saloed
force-pushed
the
misonijnik/4.5-anyfield-cleanup
branch
from
August 19, 2026 23:10
d6a608f to
1caf0c6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.