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
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public ResponseEntity<GenericVulnerabilityResponseBean<String>> getVulnerablePay
HttpServletRequest request) {
String responseContent = buildLevel1Response(banner);
return buildCachedResponse(
buildRouteOnlyCacheKey(request),
buildRouteAndBannerCacheKey(request, banner),
responseContent,
resolvePublicCacheControl(browserCache),
true);
Expand Down Expand Up @@ -175,11 +175,11 @@ public ResponseEntity<GenericVulnerabilityResponseBean<String>> getSecurePayload
}

private String buildLevel1Response(String banner) {
String unsafeBanner = StringUtils.defaultIfBlank(banner, DEFAULT_BANNER);
String safeBanner = StringEscapeUtils.escapeHtml4(normalizeBanner(banner));
return "<section class=\"cache-poisoning-response\">"
+ "<h3>Shared Cache Response</h3>"
+ "<p><strong>Current Banner:</strong> "
+ unsafeBanner
+ safeBanner
+ "</p>"
+ "<p>The application reflects the <code>banner</code> parameter, but the cache only uses the route as the key.</p>"
+ "<p>Try poisoning the banner and see if it persists for other requests.</p>"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ void setUp() {
}

@Test
@DisplayName("Level 1 - Attacker poisons the route cache and victim gets a cached hit")
void level1ShouldPoisonSharedCacheAcrossRequests() {
@DisplayName("Level 1 - An attacker banner is isolated from a plain request")
void level1ShouldNotPoisonPlainRequests() {
ResponseEntity<GenericVulnerabilityResponseBean<String>> attackerResponse =
cachePoisoningVulnerability.getVulnerablePayloadLevel1(
ATTACKER_BANNER, true, createLevel1Request("poison"));
Expand All @@ -75,13 +75,15 @@ void level1ShouldPoisonSharedCacheAcrossRequests() {
victimResponse
.getHeaders()
.getFirst(CachePoisoningVulnerability.CACHE_STATUS_HEADER))
.isEqualTo(CachePoisoningVulnerability.CACHE_STATUS_HIT);
assertThat(victimResponse.getBody().getContent()).contains(ATTACKER_BANNER);
.isEqualTo(CachePoisoningVulnerability.CACHE_STATUS_MISS);
assertThat(victimResponse.getBody().getContent())
.contains(SAFE_BANNER)
.doesNotContain(ATTACKER_BANNER);
}

@Test
@DisplayName("Level 1 - Cache key ignores the banner query parameter")
void level1ShouldUseOnlyRouteAsCacheKey() {
@DisplayName("Level 1 - Cache key includes the banner query parameter")
void level1ShouldSeparateDifferentBanners() {
ResponseEntity<GenericVulnerabilityResponseBean<String>> firstResponse =
cachePoisoningVulnerability.getVulnerablePayloadLevel1(
"FIRST BANNER", true, createLevel1Request("FIRST"));
Expand All @@ -93,19 +95,19 @@ void level1ShouldUseOnlyRouteAsCacheKey() {
firstResponse
.getHeaders()
.getFirst(CachePoisoningVulnerability.CACHE_KEY_HEADER))
.isEqualTo(LEVEL_1_PATH);
.isEqualTo(LEVEL_1_PATH + "|banner=FIRST BANNER");
assertThat(
secondResponse
.getHeaders()
.getFirst(CachePoisoningVulnerability.CACHE_KEY_HEADER))
.isEqualTo(LEVEL_1_PATH);
.isEqualTo(LEVEL_1_PATH + "|banner=SECOND BANNER");
assertThat(
secondResponse
.getHeaders()
.getFirst(CachePoisoningVulnerability.CACHE_STATUS_HEADER))
.isEqualTo(CachePoisoningVulnerability.CACHE_STATUS_HIT);
assertThat(secondResponse.getBody().getContent()).contains("FIRST BANNER");
assertThat(secondResponse.getBody().getContent()).doesNotContain("SECOND BANNER");
.isEqualTo(CachePoisoningVulnerability.CACHE_STATUS_MISS);
assertThat(secondResponse.getBody().getContent()).contains("SECOND BANNER");
assertThat(secondResponse.getBody().getContent()).doesNotContain("FIRST BANNER");
}

@Test
Expand Down Expand Up @@ -134,8 +136,8 @@ void level1ShouldEmitSharedOnlyCacheControlWhenBrowserCacheDisabled() {
}

@Test
@DisplayName("Level 1 - Reflected script payload is cached and replayed to the victim")
void level1ShouldCacheReflectedScriptPayloadForVictim() {
@DisplayName("Level 1 - Reflected script payload is escaped and isolated")
void level1ShouldEscapeAndIsolateReflectedScriptPayload() {
ResponseEntity<GenericVulnerabilityResponseBean<String>> attackerResponse =
cachePoisoningVulnerability.getVulnerablePayloadLevel1(
XSS_SCRIPT_BANNER, true, createLevel1Request("xss"));
Expand All @@ -144,15 +146,18 @@ void level1ShouldCacheReflectedScriptPayloadForVictim() {
null, true, createLevel1Request(null));

assertValidResponse(attackerResponse);
assertThat(attackerResponse.getBody().getContent()).contains(XSS_SCRIPT_BANNER);
assertThat(attackerResponse.getBody().getContent())
.contains("&lt;script&gt;alert(document.cookie)&lt;/script&gt;")
.doesNotContain(XSS_SCRIPT_BANNER);

assertValidResponse(victimResponse);
assertThat(
victimResponse
.getHeaders()
.getFirst(CachePoisoningVulnerability.CACHE_STATUS_HEADER))
.isEqualTo(CachePoisoningVulnerability.CACHE_STATUS_HIT);
assertThat(victimResponse.getBody().getContent()).contains(XSS_SCRIPT_BANNER);
.isEqualTo(CachePoisoningVulnerability.CACHE_STATUS_MISS);
assertThat(victimResponse.getBody().getContent()).contains(SAFE_BANNER);
assertThat(victimResponse.getBody().getContent()).doesNotContain(XSS_SCRIPT_BANNER);
}

@Test
Expand Down