diff --git a/agent_api/src/main/java/dev/aikido/agent_api/collectors/WebResponseCollector.java b/agent_api/src/main/java/dev/aikido/agent_api/collectors/WebResponseCollector.java index c48c1d66..8f48cb39 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/collectors/WebResponseCollector.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/collectors/WebResponseCollector.java @@ -29,7 +29,7 @@ public static void report(int statusCode) { } // Check for attack waves - if (AttackWaveDetectorStore.check(context)) { + if (AttackWaveDetectorStore.check(context, statusCode)) { AttackQueue.add( DetectedAttackWave.createAPIEvent(context) ); diff --git a/agent_api/src/main/java/dev/aikido/agent_api/storage/attack_wave_detector/AttackWaveDetector.java b/agent_api/src/main/java/dev/aikido/agent_api/storage/attack_wave_detector/AttackWaveDetector.java index e3d6cef9..e8178faf 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/storage/attack_wave_detector/AttackWaveDetector.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/storage/attack_wave_detector/AttackWaveDetector.java @@ -44,10 +44,11 @@ public AttackWaveDetector(int attackWaveThreshold, long attackWaveTimeFrame, /** * Checks if the request is part of an attack wave. * - * @param ctx the context object to check. + * @param ctx the context object to check. + * @param statusCode the HTTP response status code. * @return true if an attack wave is detected and should be reported. */ - public boolean check(ContextObject ctx) { + public boolean check(ContextObject ctx, int statusCode) { String ip = ctx.getRemoteAddress(); if (ip == null) { return false; @@ -59,7 +60,7 @@ public boolean check(ContextObject ctx) { return false; } - if (!isWebScanner(ctx)) { + if (!isWebScanner(ctx, statusCode)) { return false; } diff --git a/agent_api/src/main/java/dev/aikido/agent_api/storage/attack_wave_detector/AttackWaveDetectorStore.java b/agent_api/src/main/java/dev/aikido/agent_api/storage/attack_wave_detector/AttackWaveDetectorStore.java index 6b7b0da8..34ea16d4 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/storage/attack_wave_detector/AttackWaveDetectorStore.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/storage/attack_wave_detector/AttackWaveDetectorStore.java @@ -15,10 +15,10 @@ public final class AttackWaveDetectorStore { private AttackWaveDetectorStore() { } - public static boolean check(ContextObject ctx) { + public static boolean check(ContextObject ctx, int statusCode) { mutex.lock(); try { - return detector.check(ctx); + return detector.check(ctx, statusCode); } catch (Throwable e) { logger.debug("An error occurred checking for attack waves: %s", e.getMessage()); return false; diff --git a/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/attack_wave_detection/PathChecker.java b/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/attack_wave_detection/PathChecker.java index a8ac863d..db748e73 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/attack_wave_detection/PathChecker.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/attack_wave_detection/PathChecker.java @@ -2,6 +2,8 @@ import dev.aikido.agent_api.helpers.ArrayHelpers; +import java.util.Set; + import static dev.aikido.agent_api.helpers.ArrayHelpers.containsIgnoreCase; import static dev.aikido.agent_api.vulnerabilities.attack_wave_detection.Paths.*; @@ -9,7 +11,12 @@ public final class PathChecker { private PathChecker() { } - public static boolean isWebScanPath(String path) { + // Extensions that a Java app would not normally serve — only count as scan hits on 404 + private static final Set FOREIGN_EXTENSIONS = Set.of( + "php", "php3", "php4", "php5", "phtml" + ); + + public static boolean isWebScanPath(String path, int statusCode) { String normalized = path.toLowerCase(); String[] segments = normalized.split("/"); String filename = ArrayHelpers.lastElement(segments); @@ -25,6 +32,11 @@ public static boolean isWebScanPath(String path) { if (containsIgnoreCase(FILE_EXTENSIONS, ext)) { return true; } + // Foreign extensions (e.g. PHP) are only suspicious when the response is 404. + // A 200 may mean the Java app is proxying to another backend. + if (FOREIGN_EXTENSIONS.contains(ext) && statusCode == 404) { + return true; + } } } diff --git a/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/attack_wave_detection/WebScanDetector.java b/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/attack_wave_detection/WebScanDetector.java index 26eb1782..bd281dcf 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/attack_wave_detection/WebScanDetector.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/attack_wave_detection/WebScanDetector.java @@ -9,14 +9,14 @@ public final class WebScanDetector { private WebScanDetector() {} - public static boolean isWebScanner(ContextObject ctx) { + public static boolean isWebScanner(ContextObject ctx, int statusCode) { String method = ctx.getMethod(); if (method != null && isWebScanMethod(method)) { return true; } String route = ctx.getRoute(); - if (route != null && isWebScanPath(route)) { + if (route != null && isWebScanPath(route, statusCode)) { return true; } diff --git a/agent_api/src/test/java/attack_wave_detection/AttackWaveDetectorTest.java b/agent_api/src/test/java/attack_wave_detection/AttackWaveDetectorTest.java index 1262af72..cd29324c 100644 --- a/agent_api/src/test/java/attack_wave_detection/AttackWaveDetectorTest.java +++ b/agent_api/src/test/java/attack_wave_detection/AttackWaveDetectorTest.java @@ -21,7 +21,7 @@ private static boolean checkDetector(AttackWaveDetector detector, String ip, boo ctx = new EmptySampleContextObject("../etc/passwd", "/wp-config.php", "BADMETHOD"); } ctx.setIp(ip); - return detector.check(ctx); + return detector.check(ctx, 404); } @Test @@ -129,13 +129,13 @@ void testSlowWebScannerThirdInterval() throws InterruptedException { @Test void testItRespectsSamplesLimit() { AttackWaveDetector detector = newAttackWaveDetector(); - detector.check(new EmptySampleContextObject("", "/../etc/passwd", "GET")); - detector.check(new EmptySampleContextObject("", "/../etc/passwd", "GET")); - detector.check(new EmptySampleContextObject("", "/test2", "GET")); - detector.check(new EmptySampleContextObject("", "/../etc/passwd", "POST")); - detector.check(new EmptySampleContextObject("", "/test3", "PUT")); - detector.check(new EmptySampleContextObject("", "/.env", "GET")); - detector.check(new EmptySampleContextObject("", "/test4", "BADMETHOD")); + detector.check(new EmptySampleContextObject("", "/../etc/passwd", "GET"), 404); + detector.check(new EmptySampleContextObject("", "/../etc/passwd", "GET"), 404); + detector.check(new EmptySampleContextObject("", "/test2", "GET"), 404); + detector.check(new EmptySampleContextObject("", "/../etc/passwd", "POST"), 404); + detector.check(new EmptySampleContextObject("", "/test3", "PUT"), 404); + detector.check(new EmptySampleContextObject("", "/.env", "GET"), 404); + detector.check(new EmptySampleContextObject("", "/test4", "BADMETHOD"), 404); assertArrayEquals( List.of( new AttackWaveDetector.Sample("GET", "https://example.com/../etc/passwd"), diff --git a/agent_api/src/test/java/attack_wave_detection/PathCheckerTest.java b/agent_api/src/test/java/attack_wave_detection/PathCheckerTest.java index 0df3a917..e6eb8587 100644 --- a/agent_api/src/test/java/attack_wave_detection/PathCheckerTest.java +++ b/agent_api/src/test/java/attack_wave_detection/PathCheckerTest.java @@ -18,7 +18,7 @@ void testIsWebScanPath_WithDangerousPaths_ReturnsTrue() { }; for (String path : dangerousPaths) { assertTrue( - isWebScanPath(path), + isWebScanPath(path, 404), "Expected '" + path + "' to be detected as a web scan path" ); } @@ -32,9 +32,33 @@ void testIsNotWebScanPath_WithSafePaths_ReturnsFalse() { }; for (String path : safePaths) { assertFalse( - isWebScanPath(path), + isWebScanPath(path, 404), "Expected '" + path + "' to NOT be detected as a web scan path" ); } } + + @Test + void testForeignExtensions_404_ReturnsTrue() { + assertTrue(isWebScanPath("/admin.php", 404), + "php extension with 404 should be a scan path"); + assertTrue(isWebScanPath("/config.php5", 404), + "php5 extension with 404 should be a scan path"); + assertTrue(isWebScanPath("/index.php3", 404), + "php3 extension with 404 should be a scan path"); + assertTrue(isWebScanPath("/index.php4", 404), + "php4 extension with 404 should be a scan path"); + assertTrue(isWebScanPath("/page.phtml", 404), + "phtml extension with 404 should be a scan path"); + } + + @Test + void testForeignExtensions_200_ReturnsFalse() { + assertFalse(isWebScanPath("/admin.php", 200), + "php extension with 200 should NOT be a scan path (app may proxy to PHP backend)"); + assertFalse(isWebScanPath("/config.php5", 200), + "php5 extension with 200 should NOT be a scan path"); + assertFalse(isWebScanPath("/page.phtml", 200), + "phtml extension with 200 should NOT be a scan path"); + } } diff --git a/agent_api/src/test/java/attack_wave_detection/WebScanDetectorBenchmarkTest.java b/agent_api/src/test/java/attack_wave_detection/WebScanDetectorBenchmarkTest.java index 5be49248..a115a97f 100644 --- a/agent_api/src/test/java/attack_wave_detection/WebScanDetectorBenchmarkTest.java +++ b/agent_api/src/test/java/attack_wave_detection/WebScanDetectorBenchmarkTest.java @@ -19,9 +19,9 @@ void testPerformance() { int iterations = 25_000; long start = System.nanoTime(); for (int i = 0; i < iterations; i++) { - WebScanDetector.isWebScanner(getTestContext("/wp-config.php", "GET", "1")); - WebScanDetector.isWebScanner(getTestContext("/vulnerable", "GET", "1'; DROP TABLE users; --")); - WebScanDetector.isWebScanner(getTestContext("/", "GET", "1")); + WebScanDetector.isWebScanner(getTestContext("/wp-config.php", "GET", "1"), 404); + WebScanDetector.isWebScanner(getTestContext("/vulnerable", "GET", "1'; DROP TABLE users; --"), 200); + WebScanDetector.isWebScanner(getTestContext("/", "GET", "1"), 200); } long end = System.nanoTime(); double timePerCheck = (double) (end - start) / iterations / 3 / 1_000_000; // Convert nanoseconds to milliseconds diff --git a/agent_api/src/test/java/attack_wave_detection/WebScanDetectorTest.java b/agent_api/src/test/java/attack_wave_detection/WebScanDetectorTest.java index eeba557d..4cce681b 100644 --- a/agent_api/src/test/java/attack_wave_detection/WebScanDetectorTest.java +++ b/agent_api/src/test/java/attack_wave_detection/WebScanDetectorTest.java @@ -20,48 +20,48 @@ private static ContextObject createTestContext(String path, String method, Map