Phase 5 of kr2s-was-i-exploited, run 2026-07-31 against Datadog.
The first attempt at this pulled raw log lines and paginated. That was the wrong approach: the MCP search tool caps each response at roughly 136–222 records, so 6,000 lines would have taken about 44 calls and still produced samples rather than answers.
These logs carry full structured attributes — @http.status_code,
@http.method, @http.url_details.path, @network.client.ip, @duration —
so every figure below is a DDSQL aggregation over the complete population. No
sampling, no pagination.
Range: 2026-07-16T00:00:00Z (oldest retained log) to 2026-07-29T22:00:00Z (just past the fix deploy at 21:02:13Z). Retention is ~15 days with no online archives.
Grouping all Active Storage traffic by endpoint, method and status returns three rows and no more, which is itself the answer to "were any requests rejected":
| Endpoint | Method | Status | Requests | Distinct IPs |
|---|---|---|---|---|
/rails/active_storage/blobs/* |
GET | 302 | 19,418 | 312 |
/rails/active_storage/representations/* |
GET | 302 | 4,874 | 345 |
/rails/active_storage/direct_uploads |
POST | 200 | 1,014 | 82 |
Not one 4xx or 5xx anywhere in Active Storage traffic over the window.
The blobs endpoint is recorded for completeness and is not an attack vector: it
redirects to S3 and serves stored bytes directly, with no variant processing, so
it never reaches libvips or matload.
1. Uploads. 1,014 across 82 IPs, all 200. Long-tailed distribution — busiest address 190, then 140, 78, 39, 37, 36 — mixed residential and campus IPv4/IPv6 behind Cloudflare. No single scripted client working a sequence.
2. Representations. 4,874 across 345 IPs, all 302 with 0 bytes. Daily volume 10–648 with dips on 07-18/19 and 07-25/26 (both weekends); peak service time stays within 82–317 ms every day. No burst, no outlier, no slow tail of the kind a large file read would produce.
Limit worth stating: a successful exploitation also returns 302. This rules out a noisy or failing attempt, not a quiet successful one.
3. Evidence-destruction paths — the highest-value query. Zero requests reached any of the six routes that resolve a blob from a client-supplied signed id, by any mutating method. Three candidates surfaced and all fall away:
- 38
PATCH /api/v1/faculty_activities/:idfrom three AWS addresses (3.145.136.244×35,3.135.197.103×2,3.137.160.76×1), all 200. No/filessegment, so this is the record update, not the files sub-resource.FacultyActivitySchemadeclares no attachment field —attachmentappears only inmapped_import_schema.rb— so the action cannot attach or purge a blob. - 187
PUTand 26PATCHon profile routes, none on theavatar,preferred_avatarorself_managed_cv_uploadsub-paths.ProfileSchemaalso declares no attachment field. - 2
PATCHmatching a%file_upload%filter resolved to/institution/faculty_evaluations/questions/multiple_file_uploads/<uuid>, an unrelated feature. Pattern false positive.
The attach-then-purge blind spot is closed for the period the logs cover.
4. Rejected requests. None. The sweep is structurally blind to rejected requests because they create no blob, so this query is the only place a second actor could appear — and it is empty.
5. Three-stage reconciliation. Does not descend, structurally rather than anomalously. Uploads (1,014) sit below representations (4,874) because most of the latter render avatars attached long before this window. The third term, surviving variant records for unattached blobs, is ~0 because the purge job removed them. The series cannot be formed on this application and its absence carries no signal.
Checksum join: not applicable. The skill's attribution path filters log lines by the checksums of crafted objects the sweep found. The sweep found none, so attribution has no starting point — not merely bounded by retention, but with nothing to anchor to.
These are Heroku router logs. They carry method, path, host, request id, client IP, dyno, service time, status and bytes. They do not carry a user agent, an authenticated account, or a request body, so attribution reaches the client address and stops there.
A clean 13-day window says nothing about the preceding 30 months.
direct_uploads.tsv and representations.tsv are partial raw-line pulls from
the first attempt (883 of 1,014 and 889 of 4,874 respectively). The aggregations
above cover the full population and supersede them. They are kept only as a
sample of the raw line format, which is what a future S3 sweep would join
against on timestamp if it ever surfaces a crafted object.