From 5e6016dfbe76dc1f8a67671b90e3dc36c1ed847c Mon Sep 17 00:00:00 2001 From: Joseph Thacker Date: Tue, 21 Oct 2025 14:13:47 -0400 Subject: [PATCH] Detect ASP.NET debugging configuration (#24) --- .../src/checks/aspnet-debugging/index.spec.ts | 57 +++++++++++++++ .../src/checks/aspnet-debugging/index.ts | 72 +++++++++++++++++++ packages/backend/src/checks/index.ts | 3 + packages/backend/src/stores/config.ts | 8 +++ 4 files changed, 140 insertions(+) create mode 100644 packages/backend/src/checks/aspnet-debugging/index.spec.ts create mode 100644 packages/backend/src/checks/aspnet-debugging/index.ts diff --git a/packages/backend/src/checks/aspnet-debugging/index.spec.ts b/packages/backend/src/checks/aspnet-debugging/index.spec.ts new file mode 100644 index 0000000..7434fbb --- /dev/null +++ b/packages/backend/src/checks/aspnet-debugging/index.spec.ts @@ -0,0 +1,57 @@ +import { createMockRequest, createMockResponse, runCheck } from "engine"; +import { describe, expect, it } from "vitest"; + +import aspNetDebugCheck from "./index"; + +describe("ASP.NET debugging check", () => { + it("detects debug true marker", async () => { + const request = createMockRequest({ + id: "req-1", + host: "example.com", + method: "GET", + path: "/web.config", + }); + + const response = createMockResponse({ + id: "res-1", + code: 200, + headers: { "content-type": ["text/xml"] }, + body: '', + }); + + const executionHistory = await runCheck(aspNetDebugCheck, [ + { request, response }, + ]); + + const findings = + executionHistory[0]?.steps[executionHistory[0].steps.length - 1] + ?.findings ?? []; + expect(findings).toHaveLength(1); + expect(findings[0]?.name).toBe("ASP.NET debugging enabled"); + }); + + it("ignores responses without marker", async () => { + const request = createMockRequest({ + id: "req-2", + host: "example.com", + method: "GET", + path: "/web.config", + }); + + const response = createMockResponse({ + id: "res-2", + code: 200, + headers: { "content-type": ["text/xml"] }, + body: '', + }); + + const executionHistory = await runCheck(aspNetDebugCheck, [ + { request, response }, + ]); + + const findings = + executionHistory[0]?.steps[executionHistory[0].steps.length - 1] + ?.findings ?? []; + expect(findings).toHaveLength(0); + }); +}); diff --git a/packages/backend/src/checks/aspnet-debugging/index.ts b/packages/backend/src/checks/aspnet-debugging/index.ts new file mode 100644 index 0000000..deb8a2d --- /dev/null +++ b/packages/backend/src/checks/aspnet-debugging/index.ts @@ -0,0 +1,72 @@ +import { defineCheck, done, Severity } from "engine"; + +import { Tags } from "../../types"; +import { keyStrategy } from "../../utils"; + +const DEBUG_MARKERS = [ + ' { + const lower = body.toLowerCase(); + return DEBUG_MARKERS.some((marker) => lower.includes(marker)); +}; + +export default defineCheck(({ step }) => { + step("detectAspNetDebug", (state, context) => { + const { response, request } = context.target; + + if (response === undefined) { + return done({ state }); + } + + const body = response.getBody()?.toText() ?? ""; + if (body.length === 0) { + return done({ state }); + } + + if (!bodyIndicatesDebugging(body)) { + return done({ state }); + } + + const description = [ + 'The response appears to expose ASP.NET debugging configuration (``).', + "", + "Running in debug mode disables important optimisations and can disclose stack traces or sensitive data.", + '**Recommendation:** Set `debug="false"` in Web.config for production deployments.', + ].join("\n"); + + return done({ + state, + findings: [ + { + name: "ASP.NET debugging enabled", + description, + severity: Severity.MEDIUM, + correlation: { + requestID: request.getId(), + locations: [], + }, + }, + ], + }); + }); + + return { + metadata: { + id: "aspnet-debugging", + name: "ASP.NET debugging enabled", + description: + 'Detects ASP.NET pages that indicate `debug="true"` in the compilation configuration.', + type: "passive", + tags: [Tags.DEBUG, Tags.INFORMATION_DISCLOSURE], + severities: [Severity.MEDIUM], + aggressivity: { minRequests: 0, maxRequests: 0 }, + }, + initState: () => ({}), + dedupeKey: keyStrategy().withHost().withPort().withPath().build(), + when: () => true, + }; +}); diff --git a/packages/backend/src/checks/index.ts b/packages/backend/src/checks/index.ts index 1c28c38..06e0ce3 100644 --- a/packages/backend/src/checks/index.ts +++ b/packages/backend/src/checks/index.ts @@ -1,5 +1,6 @@ import antiClickjackingScan from "./anti-clickjacking"; import applicationErrorsScan from "./application-errors"; +import aspNetDebuggingScan from "./aspnet-debugging"; import bigRedirectsScan from "./big-redirects"; import commandInjectionScan from "./command-injection"; import corsMisconfigScan from "./cors-misconfig"; @@ -71,6 +72,7 @@ export const Checks = { SQL_STATEMENT_IN_PARAMS: "sql-statement-in-params", SSN_DISCLOSURE: "ssn-disclosure", SUSPECT_TRANSFORM: "suspect-transform", + ASPNET_DEBUGGING: "aspnet-debugging", // MYSQL_TIME_BASED_SQLI: "mysql-time-based-sqli" - TODO: fix false positives } as const; @@ -111,5 +113,6 @@ export const checks = [ sqlStatementInParams, ssnDisclosureScan, suspectTransformScan, + aspNetDebuggingScan, // mysqlTimeBased, ] as const; diff --git a/packages/backend/src/stores/config.ts b/packages/backend/src/stores/config.ts index 6b716b7..4e01546 100644 --- a/packages/backend/src/stores/config.ts +++ b/packages/backend/src/stores/config.ts @@ -188,6 +188,10 @@ export class ConfigStore { checkID: Checks.MISSING_CONTENT_TYPE, enabled: true, }, + { + checkID: Checks.ASPNET_DEBUGGING, + enabled: false, + }, ], }, { @@ -371,6 +375,10 @@ export class ConfigStore { checkID: Checks.MISSING_CONTENT_TYPE, enabled: true, }, + { + checkID: Checks.ASPNET_DEBUGGING, + enabled: true, + }, ], }, {