fix(environment): review untyped retained form fields

This commit is contained in:
gensokyo-dev
2026-10-24 13:02:47 +00:00
parent 78bfd97517
commit 0895c89bc7
3 changed files with 59 additions and 6 deletions
@@ -389,3 +389,52 @@ it.each([
skipped: NONE_SKIPPED,
});
});
it.each([
{ text: "password=example", pointers: ["/password"] },
{ text: "token=a&page=1&token=b", pointers: ["/token", "/token"] },
{ text: "%70assword=encoded", pointers: ["/password"] },
{ text: "flag&password=example", pointers: ["/password"] },
])(
"reviews retained form names without a recorded media type: $text",
({ text, pointers }) => {
const capture = scenarioCaptureFixture(
[
{
method: "POST",
url: "/api/session",
status: 200,
mediaType: "application/json",
body: "{}",
requestBody: text,
},
],
{ request_body: true, response_body: true, header_values: true },
);
expect(
scanCredentialNames(scenarioCaptureRecords(capture, "review").records),
).toEqual({
count: pointers.length,
pointers: pointers.map((pointer) => `record 0 request ${pointer}`),
skipped: NONE_SKIPPED,
});
},
);
it("keeps JSON interpretation before an untyped form fallback and honors explicit non-form types", () => {
const json = record(7, undefined, undefined);
const plain = record(8, undefined, undefined);
(json.reported as { request: { postData: unknown } }).request.postData = {
mimeType: "",
text: '{"name":"password=example","token":"actual"}',
};
(plain.reported as { request: { postData: unknown } }).request.postData = {
mimeType: "text/plain",
text: "password=example",
};
expect(scanCredentialNames([json, plain])).toEqual({
count: 1,
pointers: ["record 7 request /token"],
skipped: NONE_SKIPPED,
});
});
@@ -103,18 +103,21 @@ const parsedBody = (
if (typeof text !== "string") return undefined;
if (cut || Buffer.byteLength(text, "utf8") > SCAN_BODY_LIMIT_BYTES)
return { kind: "skipped" };
const format = typeof mimeType === "string" ? mediaTypeEssence(mimeType) : "";
if (format === "application/x-www-form-urlencoded")
return { kind: "form", fields: [...new URLSearchParams(text)] };
const format =
typeof mimeType === "string" ? mediaTypeEssence(mimeType) : null;
const form = format === "application/x-www-form-urlencoded";
const start = text.trimStart()[0];
if (start === "{" || start === "[") {
if (!form && (start === "{" || start === "[")) {
try {
return { kind: "json", value: JSON.parse(text) };
} catch {
return undefined;
}
}
return undefined;
// An unclassified capture can still retain form text. Review its potential names without changing Evidence or manifest interpretation.
return form || (format === null && text.includes("="))
? { kind: "form", fields: [...new URLSearchParams(text)] }
: undefined;
};
/** Name/value pairs such as HAR headers and query strings. */
@@ -32,7 +32,8 @@ export const WITHHELD_WRITE_HINTS = {
"The write's path names no collection or item the capture reads. Read the collection or item it changes.",
"no-call":
"No recorded call of this tool was modelled, for example because every call failed. Record a successful call.",
"body-unusable": "The request body is not a usable JSON object.",
"body-unusable":
"The retained request or response cannot be modelled. Record a successful call with a complete supported request and reproducible response.",
"no-observation":
"Nothing showed the entity or document this write changed. Read a created entity back by id right after the write; for an update or delete the state must already hold the entity, so read it before the write or record the create that made it.",
"unexplained-change":