FetchAll swallowed per-source errors and always returned nil, so all
three upstreams failing was indistinguishable from an empty log list
and the hourly sync silently tailed nothing. Total failure now
returns an error; partial failure logs a warning.
The server had no read/write/idle timeouts, so slowloris connections
held goroutines and file descriptors forever, and the default address
bound all interfaces, exposing the API to the network unintentionally.
Defaults now match the tool's local-search use case; add timeouts and
cap header size.
Certificate SANs went into the store after only lowercasing, so a
unicode name like 'buecher.example.com' was stored as raw UTF-8 and
could never be found by search, which runs IDNA. Ingest now maps
names through a permissive IDNA profile (keeping DKIM-style
underscores that the strict lookup profile rejects), enforces RFC
1035 label lengths, and stores the canonical form. Normalize also no
longer lowercases before IDNA mapping, which produced wrong punycode
for decomposed unicode input.
fetchRange counted an entry as consumed before attempting to decode
it, so undecodable entries were skipped permanently with no record,
and the position could overshoot the requested range if a log served
more entries than asked. The position now advances by exactly the
number of received entries (clamped to the range) and skips are
logged with their index window.
The tailers ran in a fire-and-forget goroutine, so shutdown cancelled
the context and immediately closed Pebble while a tailer could still
be inside Store.Watermark or an in-flight fetch, racing db.Close.
serve now waits (up to 10s) for the tailer supervisor to exit first.
Recount mutated totalBase/pendingTotal under the store mutex while the
ingest loop read and wrote them unlocked (a data race), and left stale
per-apex count caches that corrupted subsequent increments. Recount is
now an ingest-loop operation: it flushes pending records, rescans, and
resets loop-owned state inline, so counters have a single owner.
Every distinct client key allocated a bucket retained for up to 24
hours, and keys are cheap to mint at line rate (spoofed XFF values,
IPv6 /64s), so a remote attacker could grow the map until the process
ran out of memory. Shards now evict expired buckets and then arbitrary
victims once a shard exceeds 4096 buckets.
Scan materialized every record for an apex in memory, sorted it, and
the server rebuilt the full body twice more. One query for a large
apex could buffer hundreds of megabytes three times over; a few
concurrent requests exhausted RAM. Scan now keeps only the top N
results by insertion sequence via a bounded heap (default 100000,
configurable with -max-results), preserving collection order.
The limiter indexed the XFF chain at len(trustedHops) without checking
that the trusted suffix contained valid IPs. With an over-counted
trusted-proxy-hops setting, a single garbage header entry became the
rate limit bucket and attackers could rotate it to bypass the limit.
Bogus headers now fall back to the connection address.
VerifySTH silently passed when no log key was pinned or when an STH
arrived without a signature, and a failed verification only produced a
log line while ingestion continued from the unauthenticated source.
NewClient now refuses logs without keys, the tailer skips the fetch
cycle on verification failure, and it rejects list entries whose
log_id does not match SHA-256 of the key.
NewClient returns (nil, err) when a log list entry has an undecodable
key, but the error path called client.ShortID() on the nil client,
panicking the tailer goroutine and crashing the process.
Both call sites pass the extnValue OCTET STRING contents, which per
RFC 6962 section 3.3 are already the TLS-encoded SCT list. The extra
asn1.Unmarshal failed on virtually every real certificate (the first
byte is the high byte of the uint16 length), so embedded SCT
timestamps were silently dropped and first_seen fell back to the leaf
timestamp. The test masked this by double-wrapping the value.
Certificate SANs are attacker-controlled and were copied into store
keys unfiltered. Since 0x00 is the apex/sub key separator, a SAN like
'ab\0cd.qqq' corrupted per-apex counts during recount and made scan
bounds ambiguous. Names now must be printable ASCII at ingest, and
Normalize rejects them as defense in depth.
The tailer fsynced the watermark immediately after pushing records into
the async ingest batch, so a crash could persist the marker without the
data it covers, losing those entries permanently. Watermark updates now
travel through the same channel as records and are written inside the
same Pebble batch, and never move backwards.