utas: My Squad filter corpus — every filter identified, root cause measured

Controlled retail capture, one criterion at a time, cleared between each.
47 transactions. Every filter the My Squad picker sends is now known from
the wire rather than guessed.

Route: GET /ut/game/fifa17/club -- the picker hits UTAS and reuses the
general club-inventory route.

  level=any|gold        quality      lowercase, ALWAYS present
  rare=SP               "Special"    uppercase, OMITTED when off
  position=ST           position     uppercase, omitted when off
  nation=52             entity id    numeric
  league=13             entity id    numeric
  team=5                entity id    numeric, NESTED under league
  sort=desc                          client constant; the UI has no sort control
  start=/count=11       pagination

Two encoding families: short string enums, and numeric FIFA ids. The ids
must never reach Core. Filters compose as plain ANDs in one query --
string and id filters alike -- so each maps independently.

THE ROOT CAUSE IS SELF-AMPLIFYING.

club_route honours type, team and league; it never reads start, count,
level, sort or year. Because start is ignored, every page returns the
same full set, so the client concludes the page was full and asks for the
next one. One scroll produced 22 requests and 6.2 MB, stopping at
start=200 only because the client gave up -- against a filtered set of 32
items that should have been three pages.

That also explains why the bug reads as erratic rather than broken:
league=13&position=ST returns every Premier League player instead of
Premier League strikers. Plausible, wrongly sized, hard to notice.

Measured filtered sets, from the real cluttered club -- these are the
acceptance test for the fix:

  unfiltered            1962
  league=13              350
  league=13&team=5        32

Two client behaviours worth carrying forward: the picker fires a query
per highlighted entry, not per selection (two requests for one club
pick), and parameter ORDER is not stable, so parsing must be key-value.

FIXTURE SIZE: bodies over 4 KB are truncated in the committed fixture,
with body_full_len and body_full_sha256 retained, because the same 1.1 MB
club response repeats ~25 times and its hash already proves identity.
13.3 MB -> 247 KB. The raw .ofcap keeps every byte, privately and
gitignored. Truncation is recorded per transaction so a trimmed fixture
is never mistaken for a whole response.

Audited across all three identifier surfaces -- headers, JSON bodies,
query strings -- before and after the size change: no leaks. 6/6
sanitiser mutations still killed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-08-11 19:32:57 +00:00
parent 0b66662525
commit 04c5043aba
5 changed files with 250 additions and 1 deletions
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
MARKER at-player-search 19:03:01 UTC
watermark=13
MARKER NO_FILTER 19:04:35 UTC
MARKER GOLD 19:16:44 UTC
NOTE: the FIFA 17 My Squad UI labels this filter SPECIAL, not Rare. Captured under label SPECIAL.
MARKER SPECIAL 19:21:05 UTC
MARKER POSITION_ST 19:22:00 UTC
MARKER NATION_ARGENTINA 19:22:53 UTC
MARKER LEAGUE_PREMIER 19:23:34 UTC
MARKER CLUB_CHELSEA 19:24:34 UTC
MARKER GOLD_PLUS_ST 19:25:26 UTC
MARKER LEAGUE_PLUS_ST 19:27:27 UTC
MARKER PAGINATION 19:28:49 UTC
NOTE: no sort control exists in the My Squad UI; sort=desc is a client constant.
@@ -0,0 +1,84 @@
{
"routes": {
"accountInfo": {
"body_len": 2,
"fields": {
"keys": []
},
"sha256": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
"status": 200
},
"activeSquad": {
"body_len": 8034,
"fields": {
"formation": "f442",
"id": 0,
"players.count": 23,
"slots": {
"0": 100000003,
"1": 100000006,
"10": 100000010,
"11": 0,
"12": 0,
"13": 0,
"14": 0,
"15": 0,
"16": 0,
"17": 0,
"18": 0,
"19": 0,
"2": 100000005,
"20": 0,
"21": 0,
"22": 0,
"3": 100000008,
"4": 100000007,
"5": 100000002,
"6": 100000004,
"7": 100000009,
"8": 100000001,
"9": 100000025
}
},
"sha256": "07e330ed358fbefe31379cd2462aaac4bdc9c85ee28b7500dd2d963e5ea565bd",
"status": 200
},
"credits": {
"body_len": 148,
"fields": {
"credits": 28112944
},
"sha256": "0a5f3bac80c3a8ee6f088ccf180f5fdcbcbe8a2ef19c7026e4f21876016d7786",
"status": 200
},
"tradePile": {
"body_len": 862,
"fields": {
"auctionInfo.count": 1
},
"sha256": "b42bab98202209bd8309beb0eca73dba471688e69fef3e014b59901fbc21fe04",
"status": 200
},
"unassigned": {
"body_len": 16,
"fields": {
"itemData.count": 0
},
"sha256": "873f8bba8baf9c573fc51b54d100c357b3bf0caeb2ccd104245c295e073cf342",
"status": 200
},
"userMassInfo": {
"body_len": 8929,
"fields": {
"clubAbbr": "OFC",
"clubName": "OpenFUT",
"personaId": 33068179,
"trophies": 0
},
"sha256": "a616aca1742263c47ade9409693e66ec13b50114e608d88bb94141ce80237b66",
"status": 200
}
},
"unix": 1786476617.899911,
"upstream": "127.0.0.1:8099"
}
@@ -0,0 +1,84 @@
{
"routes": {
"accountInfo": {
"body_len": 2,
"fields": {
"keys": []
},
"sha256": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
"status": 200
},
"activeSquad": {
"body_len": 8034,
"fields": {
"formation": "f442",
"id": 0,
"players.count": 23,
"slots": {
"0": 100000003,
"1": 100000006,
"10": 100000010,
"11": 0,
"12": 0,
"13": 0,
"14": 0,
"15": 0,
"16": 0,
"17": 0,
"18": 0,
"19": 0,
"2": 100000005,
"20": 0,
"21": 0,
"22": 0,
"3": 100000008,
"4": 100000007,
"5": 100000002,
"6": 100000004,
"7": 100000009,
"8": 100000001,
"9": 100000025
}
},
"sha256": "07e330ed358fbefe31379cd2462aaac4bdc9c85ee28b7500dd2d963e5ea565bd",
"status": 200
},
"credits": {
"body_len": 148,
"fields": {
"credits": 28112944
},
"sha256": "0a5f3bac80c3a8ee6f088ccf180f5fdcbcbe8a2ef19c7026e4f21876016d7786",
"status": 200
},
"tradePile": {
"body_len": 862,
"fields": {
"auctionInfo.count": 1
},
"sha256": "b42bab98202209bd8309beb0eca73dba471688e69fef3e014b59901fbc21fe04",
"status": 200
},
"unassigned": {
"body_len": 16,
"fields": {
"itemData.count": 0
},
"sha256": "873f8bba8baf9c573fc51b54d100c357b3bf0caeb2ccd104245c295e073cf342",
"status": 200
},
"userMassInfo": {
"body_len": 8929,
"fields": {
"clubAbbr": "OFC",
"clubName": "OpenFUT",
"personaId": 33068179,
"trophies": 0
},
"sha256": "a616aca1742263c47ade9409693e66ec13b50114e608d88bb94141ce80237b66",
"status": 200
}
},
"unix": 1786474768.2925124,
"upstream": "127.0.0.1:8099"
}
+21 -1
View File
@@ -44,6 +44,7 @@ committed fixture, and preserves exact bodies first, sanitises second.
"""
import argparse
import base64
import hashlib
import json
import os
import re
@@ -326,7 +327,18 @@ def cmd_parse(args):
"keepalive_observed": keepalive,
},
}
out.write(json.dumps(sanitize(rec), sort_keys=True) + "\n")
rec = sanitize(rec)
if getattr(args, "max_body", 0):
for side in ("request", "response"):
raw = base64.b64decode(rec[side]["body_b64"])
if len(raw) > args.max_body:
rec[side]["body_b64"] = base64.b64encode(
raw[: args.max_body]
).decode()
rec[side]["body_truncated"] = True
rec[side]["body_full_len"] = len(raw)
rec[side]["body_full_sha256"] = hashlib.sha256(raw).hexdigest()
out.write(json.dumps(rec, sort_keys=True) + "\n")
total += 1
os.chmod(out_path, 0o644)
@@ -567,6 +579,14 @@ def main():
p = sub.add_parser("parse")
p.add_argument("--session", required=True)
# Repository-safe size. The raw .ofcap keeps every byte privately; a
# committed fixture that repeats an identical 1.1MB response 25 times is
# 13MB of git history proving something its own sha256 already proves.
# Bodies over the limit are replaced by a bounded head sample, and the
# omission is RECORDED per transaction so nobody mistakes a truncated
# fixture for the whole response.
p.add_argument("--max-body", type=int, default=0,
help="bytes; 0 = keep every body in full")
p.set_defaults(fn=cmd_parse)
s = sub.add_parser("snapshot")