Skip to content

Commit d3e2ce0

Browse files
committed
fix: address PR review feedback
- Fix generic-api-key regex to match unquoted values - Replace manual introspection validation with Zod schema - Extract parseJsonObject/hasAnyKey helpers in laravel-debug - Rewrite wordpress-readme to defineCheckV2 with stricter validation - Remove HTML-matching regex from xml-input-detection - Disable cookie-httponly and cookie-secure in light/balanced presets
1 parent f94e662 commit d3e2ce0

11 files changed

Lines changed: 283 additions & 213 deletions

File tree

packages/backend/src/checks/generic-api-key/index.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,26 @@ describe("generic-api-key", () => {
103103
expect(history[0]?.steps[0]?.findings).toHaveLength(1);
104104
});
105105

106+
it("detects unquoted api_key value", async () => {
107+
const history = await runCheck(check, [
108+
{
109+
request: createMockRequest({
110+
id: "1",
111+
host: "example.com",
112+
method: "GET",
113+
path: "/",
114+
}),
115+
response: createMockResponse({
116+
id: "1",
117+
code: 200,
118+
body: "api_key=ABCDEFGHIJKLMNOPQRSTUVWXyz",
119+
}),
120+
},
121+
]);
122+
expect(history).toHaveLength(1);
123+
expect(history[0]?.steps[0]?.findings).toHaveLength(1);
124+
});
125+
106126
it("ignores short values", async () => {
107127
const history = await runCheck(check, [
108128
{

packages/backend/src/checks/generic-api-key/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default defineRegexCheck({
1212
tags: [Tags.SECRET],
1313
severity: Severity.LOW,
1414
patterns: [
15-
/(?:api[_-]?key|api[_-]?secret|secret[_-]?key|access[_-]?token)["']?\s{0,5}[:=]\s{0,5}["']([A-Za-z0-9_-]{20,64})["']/i,
15+
/(?:api[_-]?key|api[_-]?secret|secret[_-]?key|access[_-]?token)["']?\s{0,5}[:=]\s{0,5}["']?([A-Za-z0-9_-]{20,64})(?:["']|\b)/i,
1616
],
1717
dedupeKey: keyStrategy().withHost().withPort().withPath().build(),
1818
when: whenTextResponse,

packages/backend/src/checks/graphql/introspection/index.ts

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,23 @@
11
import { defineCheckV2, Result, Severity } from "engine";
2+
import { z } from "zod";
23

34
import { Tags } from "../../../types";
45
import { keyStrategy } from "../../../utils/key";
56

67
const INTROSPECTION_QUERY = '{"query":"{ __schema { types { name } } }"}';
78
const GET_INTROSPECTION_QUERY = "query={__schema{types{name}}}";
89

10+
const IntrospectionSchema = z.object({
11+
data: z.object({
12+
__schema: z.object({
13+
types: z.array(z.unknown()),
14+
}),
15+
}),
16+
});
17+
918
function hasIntrospectionResult(body: string): boolean {
1019
try {
11-
const parsed = JSON.parse(body) as {
12-
data?: { __schema?: { types?: unknown[] } };
13-
};
14-
15-
if (typeof parsed !== "object" || parsed === null) {
16-
return false;
17-
}
18-
19-
if (!("data" in parsed)) {
20-
return false;
21-
}
22-
23-
const data = parsed.data;
24-
if (typeof data !== "object" || data === null) {
25-
return false;
26-
}
27-
28-
if (!("__schema" in data)) {
29-
return false;
30-
}
31-
32-
const schema = data.__schema;
33-
if (typeof schema !== "object" || schema === null) {
34-
return false;
35-
}
36-
37-
return "types" in schema && Array.isArray(schema.types);
20+
return IntrospectionSchema.safeParse(JSON.parse(body)).success;
3821
} catch {
3922
return false;
4023
}

packages/backend/src/checks/laravel-debug/index.ts

Lines changed: 41 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,34 @@ type EndpointConfig = {
2020
validator: (body: string, contentType: string) => boolean;
2121
};
2222

23+
function parseJsonObject(
24+
body: string,
25+
contentType: string,
26+
): Record<string, unknown> | undefined {
27+
if (!isJsonContentType(contentType)) {
28+
return undefined;
29+
}
30+
31+
try {
32+
const parsed: unknown = JSON.parse(body);
33+
34+
if (typeof parsed !== "object" || parsed === null) {
35+
return undefined;
36+
}
37+
38+
return parsed as Record<string, unknown>;
39+
} catch {
40+
return undefined;
41+
}
42+
}
43+
44+
function hasAnyKey(
45+
value: Record<string, unknown>,
46+
keys: readonly string[],
47+
): boolean {
48+
return keys.some((key) => key in value);
49+
}
50+
2351
const LARAVEL_ENDPOINTS: EndpointConfig[] = [
2452
{
2553
path: "_ignition/health-check",
@@ -30,19 +58,10 @@ const LARAVEL_ENDPOINTS: EndpointConfig[] = [
3058
impact:
3159
"Attackers can confirm the application uses Laravel with Ignition debug mode enabled, and in vulnerable versions execute arbitrary code on the server.",
3260
validator: (body: string, contentType: string) => {
33-
if (!isJsonContentType(contentType)) {
34-
return false;
35-
}
36-
try {
37-
const parsed = JSON.parse(body) as Record<string, unknown>;
38-
return (
39-
typeof parsed === "object" &&
40-
parsed !== null &&
41-
"can_execute_commands" in parsed
42-
);
43-
} catch {
44-
return false;
45-
}
61+
const parsed = parseJsonObject(body, contentType);
62+
return (
63+
parsed !== undefined && hasAnyKey(parsed, ["can_execute_commands"])
64+
);
4665
},
4766
},
4867
{
@@ -54,22 +73,11 @@ const LARAVEL_ENDPOINTS: EndpointConfig[] = [
5473
impact:
5574
"Attackers can access detailed profiling information including SQL queries with parameters, session data, authentication details, and application configuration.",
5675
validator: (body: string, contentType: string) => {
57-
if (!isJsonContentType(contentType)) {
58-
return false;
59-
}
60-
try {
61-
const parsed = JSON.parse(body) as Record<string, unknown>;
62-
return (
63-
typeof parsed === "object" &&
64-
parsed !== null &&
65-
("id" in parsed ||
66-
"method" in parsed ||
67-
"uri" in parsed ||
68-
"time" in parsed)
69-
);
70-
} catch {
71-
return false;
72-
}
76+
const parsed = parseJsonObject(body, contentType);
77+
return (
78+
parsed !== undefined &&
79+
hasAnyKey(parsed, ["id", "method", "uri", "time"])
80+
);
7381
},
7482
},
7583
{
@@ -81,19 +89,10 @@ const LARAVEL_ENDPOINTS: EndpointConfig[] = [
8189
impact:
8290
"Attackers can view all application requests, exceptions with stack traces, database queries, log entries, and scheduled tasks.",
8391
validator: (body: string, contentType: string) => {
84-
if (!isJsonContentType(contentType)) {
85-
return false;
86-
}
87-
try {
88-
const parsed = JSON.parse(body) as Record<string, unknown>;
89-
return (
90-
typeof parsed === "object" &&
91-
parsed !== null &&
92-
("data" in parsed || "entries" in parsed || "type" in parsed)
93-
);
94-
} catch {
95-
return false;
96-
}
92+
const parsed = parseJsonObject(body, contentType);
93+
return (
94+
parsed !== undefined && hasAnyKey(parsed, ["data", "entries", "type"])
95+
);
9796
},
9897
},
9998
];

packages/backend/src/checks/slack-token-disclosure/index.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ const FAKE_SUFFIX = "FAKETESTVALUENOTREAL000000";
1010
const XOXB_TOKEN = XOXB_PREFIX + FAKE_MID + FAKE_SUFFIX;
1111
const XOXP_TOKEN = XOXP_PREFIX + FAKE_MID + FAKE_SUFFIX;
1212
const WEBHOOK_PREFIX = "https://hooks.slack.com/services/";
13-
const WEBHOOK_TOKEN = WEBHOOK_PREFIX + "T00000000/B00000000/ABCDEFGHIJKLMNOPQRSTUVWX";
13+
const WEBHOOK_TOKEN =
14+
WEBHOOK_PREFIX + "T00000000/B00000000/ABCDEFGHIJKLMNOPQRSTUVWX";
1415

1516
describe("slack-token-disclosure", () => {
1617
it("does not run on non-200 response", async () => {

0 commit comments

Comments
 (0)