Summary
CATS serializes array-valued query parameters as a single comma-joined value regardless of the
parameter's declared explode: true, so the server receives one malformed value instead of repeated
keys. TMI correctly rejects it, and CATS reports the correct rejection as a failure.
Split out of #657 (the upstream example-vs-schema defect) because it is a distinct bug with its
own blast radius, and because it now costs real triage time: it produced 10 true positives in run
20260731T200650Z, three of them from HappyPath, which is exactly the signature of a genuine
server regression.
Evidence
GET /threat_models/{id}/threats (HappyPath, test 95506) — CATS sent:
?severity=low%2Clow&priority=UKADVXSDT%2CUKADVXSDT&threat_type=KSDLBKSRNSCAZ%2CKSDLBKSRNSCAZ
The server saw a single severity value of low,low, which is not in the enum, and answered:
{"error": "invalid_id", "error_description": "error in openapi3filter.RequestError: parameter \"severity\" in query has an error: Error at \"/0\": value is not one of the allowed values [\"unknown\",\"informational\",\"low\",\"medium\",\"high\",\"critical\"]"}
DELETE /threat_models/{id}/threats/bulk (HappyPath, test 97146) — same shape:
?threat_ids=2725d0d1-1e7d-460c-825d-84aff58ff8d6%2C2725d0d1-1e7d-460c-825d-84aff58ff8d6
{"error": "invalid_input", "error_description": "Invalid bulk delete request format"}
Note CATS does build the array correctly in its own JSON view of the request
("severity":["low","low"]) — it is only the wire serialization that collapses it. Per RFC 6570 /
OpenAPI style: form, explode: true, the correct encoding is severity=low&severity=low.
Impact
Affected findings
| path |
method |
count |
/threat_models/{threat_model_id}/threats |
GET |
7 |
/threat_models/{threat_model_id}/threats/bulk |
DELETE |
3 |
Options
- Report upstream alongside Endava/cats#206, which
noted this as possibly sharing a code path. Preferred — it is a clear spec-conformance bug.
- Add a false-positive rule matching the comma-joined-array signature. Cheap, but it would also
mask a genuine 400 on these endpoints, and it does nothing about the lost coverage.
- Do nothing and re-triage the same 10 findings every campaign.
Recommend 1, with 2 only as a stopgap if the campaign noise becomes a problem before upstream moves.
Acceptance Criteria
References
Summary
CATS serializes array-valued query parameters as a single comma-joined value regardless of the
parameter's declared
explode: true, so the server receives one malformed value instead of repeatedkeys. TMI correctly rejects it, and CATS reports the correct rejection as a failure.
Split out of #657 (the upstream
example-vs-schemadefect) because it is a distinct bug with itsown blast radius, and because it now costs real triage time: it produced 10 true positives in run
20260731T200650Z, three of them fromHappyPath, which is exactly the signature of a genuineserver regression.
Evidence
GET /threat_models/{id}/threats(HappyPath, test 95506) — CATS sent:The server saw a single
severityvalue oflow,low, which is not in the enum, and answered:{"error": "invalid_id", "error_description": "error in openapi3filter.RequestError: parameter \"severity\" in query has an error: Error at \"/0\": value is not one of the allowed values [\"unknown\",\"informational\",\"low\",\"medium\",\"high\",\"critical\"]"}DELETE /threat_models/{id}/threats/bulk(HappyPath, test 97146) — same shape:{"error": "invalid_input", "error_description": "Invalid bulk delete request format"}Note CATS does build the array correctly in its own JSON view of the request
(
"severity":["low","low"]) — it is only the wire serialization that collapses it. Per RFC 6570 /OpenAPI
style: form, explode: true, the correct encoding isseverity=low&severity=low.Impact
20260731T200650Z, across 2 paths.HappyPath400 reads as a real regression until the rawURL is examined.
exercises these endpoints' array filters with valid input, so genuine bugs behind
severity,priority,threat_typeandthreat_idshandling would not be found. This is the same class ofhidden coverage loss as chore(cats): anchor-path decoys erase ~97% of the anchor's own coverage (zero 2xx on PATCH/PUT for /threat_models/{id}) #651.
Affected findings
/threat_models/{threat_model_id}/threats/threat_models/{threat_model_id}/threats/bulkOptions
noted this as possibly sharing a code path. Preferred — it is a clear spec-conformance bug.
mask a genuine 400 on these endpoints, and it does nothing about the lost coverage.
Recommend 1, with 2 only as a stopgap if the campaign noise becomes a problem before upstream moves.
Acceptance Criteria
References
example, notschema#657 — the upstreamexample-vs-schemadefect, where this was first noted20260731T200650Z, tests 95506 and 97146