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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
/Gemfile
/Gemfile.lock
/favicon.ico
_site/
_site/
.idea/
dependency-reduced-pom.xml
target/
117 changes: 117 additions & 0 deletions src/main/java/org/owasp/astf/testcases/RateLimitingTestCase.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package org.owasp.astf.testcases;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.owasp.astf.core.EndpointInfo;
import org.owasp.astf.core.http.HttpClient;
import org.owasp.astf.core.result.Finding;
import org.owasp.astf.core.result.Severity;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
* Tests for Rate Limiting vulnerabilities.
*
* This test case sends multiple requests in rapid succession to the API endpoint
* and checks if rate limiting is properly enforced.
*/
public class RateLimitingTestCase implements TestCase {

private static final Logger logger = LogManager.getLogger(RateLimitingTestCase.class);

private static final int REQUEST_COUNT = 20;
private static final List<String> SAFE_ENDPOINTS = List.of(
"/api/health",
"/api/status",
"/api/public/info"
);

@Override
public String getId() {
return "ASTF-RL-2025";
}

@Override
public String getName() {
return "Rate Limiting";
}

@Override
public String getDescription() {
return """
Sends multiple requests in a short timeframe to test if the API
enforces rate limiting to protect against brute force or abuse.
""";
}

@Override
public List<Finding> execute(EndpointInfo endpoint, HttpClient httpClient) throws IOException {
logger.info("Executing {} test on {}", getId(), endpoint);

List<Finding> findings = new ArrayList<>();
Map<String, String> headers = Map.of("Content-Type", "application/json");
String requestBody = "{\"key\":\"value\"}";
boolean rateLimitTriggered = false;

for (int i = 0; i < REQUEST_COUNT; i++) {
try {
String response;

switch (endpoint.getMethod().toUpperCase()) {
case "POST" -> response = httpClient.post(endpoint.getPath(), headers, "application/json", requestBody);
case "GET" -> response = httpClient.get(endpoint.getPath(), headers);
case "PUT" -> response = httpClient.put(endpoint.getPath(), headers, "application/json", requestBody);
case "DELETE" -> response = httpClient.delete(endpoint.getPath(), headers);
default -> {
logger.warn("Unsupported HTTP method {} for endpoint {}", endpoint.getMethod(), endpoint.getPath());
return findings;
}
}

// Check if the response indicates rate limiting
// This might be HTTP 429 Too Many Requests or some error message in response body
if (response != null && (response.contains("429") || response.toLowerCase().contains("rate limit") || response.toLowerCase().contains("too many requests"))) {
rateLimitTriggered = true;
break;
}

} catch (IOException e) {
logger.error("Request failed on attempt {}: {}", i + 1, e.getMessage());
}
}

if (rateLimitTriggered) {
findings.add(new Finding(
UUID.randomUUID().toString(),
"Rate Limiting Enforced",
"The endpoint correctly enforced rate limiting after multiple rapid requests.",
Severity.INFO,
getId(),
endpoint.getMethod() + " " + endpoint.getPath(),
"No action needed."
));
} else if (!isSafeToSkipRateLimiting(endpoint.getPath())) {
findings.add(new Finding(
UUID.randomUUID().toString(),
"Missing or Ineffective Rate Limiting",
"The endpoint did not enforce rate limiting after multiple rapid requests.",
Severity.HIGH,
getId(),
endpoint.getMethod() + " " + endpoint.getPath(),
"Implement rate limiting to protect API endpoints from brute force and abuse."
));
} else {
logger.info("Skipping finding for non-sensitive endpoint: {}", endpoint.getPath());
}

return findings;
}

private boolean isSafeToSkipRateLimiting(String path) {
return SAFE_ENDPOINTS.stream().anyMatch(path::startsWith);
}
}
101 changes: 101 additions & 0 deletions src/test/java/org/owasp/astf/testcases/RateLimitingTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package org.owasp.astf.testcases;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.owasp.astf.core.EndpointInfo;
import org.owasp.astf.core.http.HttpClient;
import org.owasp.astf.core.result.Finding;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.when;

@DisplayName("Rate Limiting Test Case Tests")
class RateLimitingTest {

@Mock
private HttpClient httpClient;

private RateLimitingTestCase testCase;

@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
testCase = new RateLimitingTestCase();
}

@ParameterizedTest(name = "{index}: {0} {1} -> expected: {3}")
@MethodSource("provideEndpoints")
@DisplayName("Should detect or skip rate limiting based on server response")
void testRateLimitingDetectionWithProvider(
String description,
EndpointInfo endpoint,
String mockResponse,
String expectedFindingTitle
) throws IOException {

final int threshold = 5;
final int totalCalls = 20;
final String path = endpoint.getPath();
final String method = endpoint.getMethod().toUpperCase();

final int[] counter = {0};
var answer = (org.mockito.stubbing.Answer<String>) invocation -> {
counter[0]++;
if (counter[0] >= threshold && mockResponse.contains("Too Many Requests")) {
return "{\"error\":\"Too Many Requests\",\"status\":429}";
}
return "{\"message\":\"ok\"}";
};

switch (method) {
case "GET" -> when(httpClient.get(eq(path), anyMap())).thenAnswer(answer);
case "POST" -> when(httpClient.post(eq(path), anyMap(), anyString(), anyString())).thenAnswer(answer);
case "PUT" -> when(httpClient.put(eq(path), anyMap(), anyString(), anyString())).thenAnswer(answer);
case "DELETE" -> when(httpClient.delete(eq(path), anyMap())).thenAnswer(answer);
default -> fail("Unsupported HTTP method in test: " + method);
}

List<Finding> findings = testCase.execute(endpoint, httpClient);

if (expectedFindingTitle != null) {
assertFalse(findings.isEmpty(), "Expected finding for: " + description);
assertEquals(expectedFindingTitle, findings.get(0).getTitle(),
"Unexpected finding title for: " + description);
} else {
assertTrue(findings.isEmpty(), "Did not expect any findings for: " + description);
}
}

private static Stream<org.junit.jupiter.params.provider.Arguments> provideEndpoints() {
return Stream.of(
org.junit.jupiter.params.provider.Arguments.of(
"Rate limiting detected via 400 error on POST",
new EndpointInfo("/api/rate-limited", "POST"),
"{\"error\":\"invalid request\",\"status\":400}",
"Missing or Ineffective Rate Limiting"
),
org.junit.jupiter.params.provider.Arguments.of(
"Rate limiting detected via 429 error on GET",
new EndpointInfo("/api/rate-limited", "GET"),
"{\"error\":\"Too Many Requests\",\"status\":429}",
"Rate Limiting Enforced"
),
org.junit.jupiter.params.provider.Arguments.of(
"Health check endpoint - safe to skip",
new EndpointInfo("/api/health", "GET"),
"{\"message\":\"ok\"}",
null
)
);
}
}