Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/backend/src/checks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import sqlStatementInParams from "./sql-statement-in-params";
import ssnDisclosureScan from "./ssn-disclosure";
import sstiScan from "./ssti";
import suspectTransformScan from "./suspect-transform";
import xssFilterDisabledScan from "./xss-filter-disabled";

export type CheckID = (typeof Checks)[keyof typeof Checks];
export const Checks = {
Expand Down Expand Up @@ -71,6 +72,7 @@ export const Checks = {
SQL_STATEMENT_IN_PARAMS: "sql-statement-in-params",
SSN_DISCLOSURE: "ssn-disclosure",
SUSPECT_TRANSFORM: "suspect-transform",
XSS_FILTER_DISABLED: "xss-filter-disabled",
// MYSQL_TIME_BASED_SQLI: "mysql-time-based-sqli" - TODO: fix false positives
} as const;

Expand Down Expand Up @@ -111,5 +113,6 @@ export const checks = [
sqlStatementInParams,
ssnDisclosureScan,
suspectTransformScan,
xssFilterDisabledScan,
// mysqlTimeBased,
] as const;
61 changes: 61 additions & 0 deletions packages/backend/src/checks/xss-filter-disabled/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { createMockRequest, createMockResponse, runCheck } from "engine";
import { describe, expect, it } from "vitest";

import xssFilterDisabledCheck from "./index";

describe("X-XSS-Protection disabled check", () => {
it("flags header disabling filter", async () => {
const request = createMockRequest({
id: "req-1",
host: "example.com",
method: "GET",
path: "/",
});

const response = createMockResponse({
id: "res-1",
code: 200,
headers: {
"x-xss-protection": ["0"],
},
body: "OK",
});

const executionHistory = await runCheck(xssFilterDisabledCheck, [
{ request, response },
]);

const findings =
executionHistory[0]?.steps[executionHistory[0].steps.length - 1]
?.findings ?? [];
expect(findings).toHaveLength(1);
expect(findings[0]?.name).toBe("Browser XSS filter disabled");
});

it("ignores safe header values", async () => {
const request = createMockRequest({
id: "req-2",
host: "example.com",
method: "GET",
path: "/",
});

const response = createMockResponse({
id: "res-2",
code: 200,
headers: {
"x-xss-protection": ["1; mode=block"],
},
body: "OK",
});

const executionHistory = await runCheck(xssFilterDisabledCheck, [
{ request, response },
]);

const findings =
executionHistory[0]?.steps[executionHistory[0].steps.length - 1]
?.findings ?? [];
expect(findings).toHaveLength(0);
});
});
69 changes: 69 additions & 0 deletions packages/backend/src/checks/xss-filter-disabled/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { defineCheck, done, Severity } from "engine";

import { Tags } from "../../types";
import { keyStrategy } from "../../utils";

const isFilterDisabled = (values: Array<string> | undefined): boolean => {
if (!values) return false;

return values.some((value) => {
const normalized = value.trim().toLowerCase();
if (normalized === "0") return true;
if (normalized.startsWith("0;")) return true;
return normalized.includes("mode=0");
});
};

export default defineCheck(({ step }) => {
step("detectDisabledFilter", (state, context) => {
const { response, request } = context.target;
if (!response) {
return done({ state });
}

const headerValues = response.getHeader("x-xss-protection");
if (!isFilterDisabled(headerValues)) {
return done({ state });
}

const description = [
"The response disables the legacy browser XSS filter via the `X-XSS-Protection` header.",
"",
`**Header value:** \`${headerValues?.join(", ") ?? ""}\``,
"",
"While modern browsers ignore this header, disabling the filter can expose older clients to reflected XSS.",
"**Recommendation:** Remove the header or set it to `1; mode=block` for legacy compatibility.",
].join("\n");

return done({
state,
findings: [
{
name: "Browser XSS filter disabled",
description,
severity: Severity.LOW,
correlation: {
requestID: request.getId(),
locations: [],
},
},
],
});
});

return {
metadata: {
id: "xss-filter-disabled",
name: "Browser XSS filter disabled",
description:
"Detects responses that disable the legacy XSS filter using the X-XSS-Protection header.",
type: "passive",
tags: [Tags.SECURITY_HEADERS, Tags.XSS],
severities: [Severity.LOW],
aggressivity: { minRequests: 0, maxRequests: 0 },
},
initState: () => ({}),
dedupeKey: keyStrategy().withHost().withPort().withPath().build(),
when: () => true,
};
});
8 changes: 8 additions & 0 deletions packages/backend/src/stores/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,10 @@ export class ConfigStore {
checkID: Checks.MISSING_CONTENT_TYPE,
enabled: true,
},
{
checkID: Checks.XSS_FILTER_DISABLED,
enabled: false,
},
],
},
{
Expand Down Expand Up @@ -371,6 +375,10 @@ export class ConfigStore {
checkID: Checks.MISSING_CONTENT_TYPE,
enabled: true,
},
{
checkID: Checks.XSS_FILTER_DISABLED,
enabled: true,
},
],
},
{
Expand Down