| Age | Commit message (Collapse) | Author | Files | Lines |
|
Three parts and no message/rfc822: the original carries every identifier the
first property keeps out, and text/rfc822-headers is the standard's own
answer for a report that cannot include the message.
The third part filters the manifest's headers against the same whitelist
parse.report_headers() applies, rather than trusting it. The structural
argument is that report cannot disclose what it was never given, and a
manifest is a file the user edits: that is exactly a way it can be given a
To. parse still decides; this refuses to publish what it did not decide for.
A header value is attacker-supplied free text the spec keeps deliberately, so
a newline in a Subject forges a header line in a part read entirely as
headers. RFC 2047 turns the break into a fold instead, and a parser unfolds it
back to one header with the value intact. Applied only when a value carries
something unsafe, since encoding every header costs the desk the legibility
this part exists for.
From is built with headerregistry.Address: "Example Consulting, Ltd" through
an f-string parses back as two addresses, the first a bogus addr-spec with no
domain, so a desk's reply reaches nobody.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xj1ayFRSUQ2u7cwb3S4axE
|
|
An RFC 5965 envelope carrying x-arf fields. 5965 reports are about a message
and these are about indicators, so x-arf's Source fills the gap while the
report-type keeps a standards parser working.
Field values are percent-encoded against header injection. This is reachable,
not theoretical: redact.url_valued_parameters() URL-decodes a redirector's
destination to recover it as an indicator, so a body carrying
"?next=http%3A%2F%2Fa.invalid%2Fx%0AFeedback-Type%3A%20not-abuse" produces,
through parse.iocs() on a real message, an IOC whose value holds a literal
newline followed by text shaped like a field. Emitted verbatim it forges a
field in a report carrying the reporter's identity. Encoding rather than
dropping keeps a genuine redirect target; encoding rather than stripping
keeps the indicator recoverable, since a stripped URL is a different URL a
desk would then act on. The escape covers everything str.splitlines() breaks
on, because Python's own email module raises on U+2028 as readily as on LF,
and it runs over UTF-8 bytes and escapes "%" so the reversal is a true
inverse rather than a second injection one step later.
Four corrections to the plan. Source-IP is emitted once, not once per IP:
RFC 5965 gives it "once maximum" and a repeat displaces the primary rather
than adding an address. Source is omitted when there is no typed indicator
rather than emitted empty, since an empty field asserts that the reported
thing is the empty string. Arrival-Date is dropped rather than copied from
the sender's Date header, which is attacker-controlled and means when the
sender claimed to send, not when our MTA received. sha256 and observation
are left to the text part rather than forced into the nearest field.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xj1ayFRSUQ2u7cwb3S4axE
|
|
The marker was an unescaped trailing backslash, and a backslash is legal in
a URL path, so a value ending in one was indistinguishable from a wrap. An
attacker who read this source could append one and make their own indicator
garble itself in the report an abuse desk reads: an adversarial trigger on
attacker-supplied text, not an edge case.
The short case needed no wrapping at all to corrupt. unwrap() ate the
following line regardless, merging an indicator with its own origin
annotation. On Subject it was worse, absorbing the Date beneath it and
making the "Message as declared" block misstate what the message declared,
which is the one thing that block exists to report faithfully.
Every backslash is now doubled before wrapping and halved on the way back,
and unwrap() tells a marker from content by the PARITY of the trailing run.
Doubling only a trailing one would leave "x\\" encoding as "x\" plus a
marker, the same bug one character along.
A second defect surfaced only under a randomised sweep, after the first fix
and a green suite: a break landing BETWEEN the halves of an escaped pair
splits the run whose parity unwrap() counts, so a real marker reads as
content and the tail is silently dropped. It needs a backslash at exactly
the break column, so no hand-written case found it and 454 of 3538 random
ones did. The wrap now backs off a character rather than splitting a pair.
Verified over 9132 adversarial values, including every backslash pattern up
to length 9 and values that are entirely backslashes: 0 round-trip failures,
0 header-block corruptions, no line over 72 columns.
Values are escaped, never rejected or sanitised: the report says what the
message contained.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xj1ayFRSUQ2u7cwb3S4axE
|
|
The ask goes first, only this desk's own indicators appear, and the redaction
note is unconditional: a desk seeing REDACTED with no explanation may read
the report as doctored.
Nothing is truncated. The plan cut three lines with a [:72] slice and left
the indicator list unwrapped, which is the same defect twice: a cut URL is a
WRONG indicator rather than a short one, and a desk acting on the first 72
characters acts on a resource nobody reported. Long values are divided with
an explicit trailing-backslash continuation instead, and unwrap() is the
exact inverse, so the tests assert reassembly rather than mere presence.
That is what rules out a truncation passing as a wrap.
Origins are mapped to English. "header-list_unsubscribe" is a parser's
vocabulary and reads as debug output; an origin the table does not know is
shown as-is, because losing the only line saying where an indicator came
from is worse than showing an ugly token.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xj1ayFRSUQ2u7cwb3S4axE
|
|
Not an error and not an exit code: a case where nothing resolved still
reaches MISP and the vendors. Visible beats absent, so review can see it
without diffing IOC lists.
Two departures from the plan, both about the pair of lists agreeing.
An indicator is unreportable only when NONE of its contacts produced a
mailable address, not when any one of them failed. Contacts fold by host,
so an indicator can sit in a domain contact that resolved and an IP
contact that did not; listing it regardless puts it in the destinations
and in the "no desk found" list at once, and a reviewer acting on the
second hand-reports something already on its way to a desk.
An abuse value with no "@", or with either half empty, no longer becomes
a destination. RDAP jCard data is third-party and occasionally malformed,
and such a value produced a pending destination with an unsendable
target, which the unreportable array then skipped because the contact
did have an abuse entry. Both functions now apply one mailability test,
so the two lists partition the indicators exactly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xj1ayFRSUQ2u7cwb3S4axE
|
|
"Abuse@Host.Invalid" and "abuse@host.invalid" produced two destinations and
two mails to one desk, which is the duplicate-mail behaviour the grouping
exists to prevent.
The root cause is the test, not the grouping. _group_key folded the domain
correctly, but the test written for it used a lowercase local part in both
of its inputs, so it varied only the half that worked and passed while the
other half was never exercised. A test that varies one half of its input
proves nothing about the other.
The local part now folds for the RFC 2142 role mailboxes. That RFC mandates
those names and requires them matched case-insensitively, so no host runs
"Abuse@" and "abuse@" as different desks and treating them as two costs a
duplicate mail with nothing on the other side of the trade. This reverses
the earlier decision only for the standardised names: any other local part
is still left as published, because for a named mailbox folding could merge
two desks a host genuinely distinguishes, and a dropped desk is worse than a
duplicate. Each half now folds on the strength of its own standard.
Both folds are mutation-checked and killed by distinct tests, so neither
masks the other, and the role fold is checked across all five role names.
Also pins two behaviours that held only by luck: the caller's contacts are
not modified, and a destination's ioc list is not aliased to the contact's.
Both are true today because the grouping starts a fresh list, and nothing
would have caught an implementation that reused the contact's.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xj1ayFRSUQ2u7cwb3S4axE
|
|
A positional id names a slot in one run's list, and report may run again on
a case whose contacts have changed since. A newly resolved indicator whose
desk sorts ahead renumbers every desk after it, so bodies/<id>.xarf on disk
comes to belong to a different desk than the manifest entry sharing that id.
The body hash recorded per destination would then compare one desk's body
against another's, reporting an edit nobody made or missing one that was.
Hashing the address makes an id follow the desk. It hashes the same
normalised form the grouping uses, so two spellings of one desk share an id
rather than letting whichever spelling RDAP published first decide a body's
filename. Eight hex chars is a deliberate ceiling: collision probability is
about 1e-8 at ten desks, and a short id keeps a case directory readable to
the person reviewing it.
Also strengthens two tests that passed against trivially wrong code: the
no-address case now asserts alongside a contact that does resolve, so
returning nothing at all is no longer a passing answer, and the two-address
case asserts the ioc lists and distinct ids rather than the sorted targets
alone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xj1ayFRSUQ2u7cwb3S4axE
|
|
Two contacts can resolve to the same desk, an IP and a domain at one hoster
being the common case, and grouping per contact would send that desk two
mails about one incident.
The domain half of an address is folded to lowercase before grouping, since
it is case-insensitive by definition; the local part is left verbatim,
because only the receiving host knows whether it folds, and folding two
desks a host distinguishes would silently drop one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xj1ayFRSUQ2u7cwb3S4axE
|