Skip to content

Commit 30830e2

Browse files
paradoxenginecopybara-github
authored andcommitted
Fix the hardcoded cookie value, which was might have been discarded as being outside the acceptable time window by the flask frontend and thus lead to false negatives.
PiperOrigin-RevId: 929186392 Change-Id: I566d5a338fb6f32a013e046d8ac8b216aff5cec4
1 parent 65a639b commit 30830e2

2 files changed

Lines changed: 87 additions & 1 deletion

File tree

community/detectors/apache_airflow_cve_2020_17526/src/main/java/com/google/tsunami/plugins/cve202017526/Cve202017526Detector.java

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,13 @@
5050
import com.google.tsunami.proto.Vulnerability;
5151
import com.google.tsunami.proto.VulnerabilityId;
5252
import java.io.IOException;
53+
import java.math.BigInteger;
5354
import java.net.HttpCookie;
5455
import java.net.URLEncoder;
5556
import java.time.Clock;
5657
import java.time.Duration;
5758
import java.time.Instant;
59+
import java.util.Base64;
5860
import java.util.HashMap;
5961
import java.util.List;
6062
import java.util.Map;
@@ -200,6 +202,38 @@ private boolean isServiceVulnerable(NetworkService networkService) {
200202
}
201203
}
202204

205+
private String toTimestamp(long l) {
206+
if (l < 0) {
207+
throw new IllegalArgumentException("Timestamp must be non-negative");
208+
}
209+
if (l == 0) {
210+
return "";
211+
}
212+
byte[] bytes = BigInteger.valueOf(l).toByteArray();
213+
// Flask uses the base64 library to sign session cookies, which encodes timestamps
214+
// by taking the Unix epoch seconds, converting it to the minimal number of bytes
215+
// in big-endian order, and then encoding those bytes using URL-safe Base64.
216+
//
217+
// The equivalent of Python's `int_to_bytes` in Java is tricky. `BigInteger.toByteArray()`
218+
// produces a two's-complement representation in big-endian order. For positive numbers,
219+
// this representation must have a 0 sign bit. If a positive number's minimal byte
220+
// representation would start with a byte >= 0x80 (i.e., MSB is 1), `toByteArray()`
221+
// prepends an extra 0x00 byte to ensure the number is interpreted as positive.
222+
//
223+
// For example, timestamp 2147483648 (0x80000000) is encoded by base64 using 4 bytes
224+
// `\x80\x00\x00\x00`, but `BigInteger.toByteArray()` returns 5 bytes: `\x00\x80\x00\x00\x00`.
225+
//
226+
// Since base64 does not expect this leading 0x00, we must remove it when present
227+
// to ensure compatibility. The condition `bytes[0] == 0 && bytes.length > 1` detects
228+
// and removes this extra byte.
229+
if (bytes[0] == 0 && bytes.length > 1) {
230+
byte[] tmp = new byte[bytes.length - 1];
231+
System.arraycopy(bytes, 1, tmp, 0, bytes.length - 1);
232+
bytes = tmp;
233+
}
234+
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
235+
}
236+
203237
private Map<String, String> getFreshCsrfTokenAndSessionCookie(NetworkService networkService)
204238
throws IOException {
205239
String rootUrl = NetworkServiceUtils.buildWebApplicationRootUrl(networkService);
@@ -208,7 +242,7 @@ private Map<String, String> getFreshCsrfTokenAndSessionCookie(NetworkService net
208242
FlaskSessionSigner newToken =
209243
new FlaskSessionSigner(
210244
"{\"_fresh\":true,\"user_id\":1,\"_permanent\":true}",
211-
"Zzx63w",
245+
toTimestamp(utcClock.instant().getEpochSecond()),
212246
"temporary_key",
213247
"cookie-session");
214248

community/detectors/apache_airflow_cve_2020_17526/src/test/java/com/google/tsunami/plugins/cve202017526/Cve202017526DetectorTest.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import com.google.tsunami.proto.TargetInfo;
4040
import java.io.IOException;
4141
import java.security.SecureRandom;
42+
import java.time.Duration;
4243
import java.time.Instant;
4344
import java.util.Arrays;
4445
import java.util.Objects;
@@ -118,6 +119,8 @@ public void detect_withCallbackServer_onVulnerableTarget_returnsVulnerability()
118119
detector.detect(targetInfo, ImmutableList.of(targetNetworkService));
119120

120121
Truth.assertThat(mockCallbackServer.getRequestCount()).isEqualTo(1);
122+
// The cookie timestamp is generated from fakeUtcClock, which is 2020-01-01T00:00:00.00Z
123+
// (1577836800). Itsdangerous encodes this as 'XgvhAA'.
121124
assertThat(detectionReports.getDetectionReportsList())
122125
.comparingExpectedFieldsOnly()
123126
.containsExactly(
@@ -178,6 +181,55 @@ public void detect_withoutCallbackServer_returnsEmpty() throws IOException {
178181
assertThat(detectionReports.getDetectionReportsList()).isEmpty();
179182
}
180183

184+
@Test
185+
public void detect_whenTimeAdvanced_usesNewTimestampInCookie()
186+
throws IOException, InterruptedException {
187+
startMockWebServer();
188+
createInjector();
189+
mockCallbackServer.enqueue(PayloadTestHelper.generateMockSuccessfulCallbackResponse());
190+
var unused = fakeUtcClock.advance(Duration.ofSeconds(1));
191+
NetworkService targetNetworkService =
192+
NetworkService.newBuilder()
193+
.setNetworkEndpoint(
194+
forHostnameAndPort(mockTargetService.getHostName(), mockTargetService.getPort()))
195+
.addSupportedHttpMethods("POST")
196+
.build();
197+
TargetInfo targetInfo =
198+
TargetInfo.newBuilder()
199+
.addNetworkEndpoints(targetNetworkService.getNetworkEndpoint())
200+
.build();
201+
202+
var unused2 = detector.detect(targetInfo, ImmutableList.of(targetNetworkService));
203+
204+
RecordedRequest request = mockTargetService.takeRequest();
205+
Truth.assertThat(request.getPath()).isEqualTo("/admin/");
206+
// After advancing the clock by 1 second, the timestamp becomes 1577836801, which
207+
// base64ncodes as 'XgvhAQ'.
208+
Truth.assertThat(request.getHeader("Cookie")).contains(".XgvhAQ.");
209+
}
210+
211+
@Test
212+
public void toTimestamp_coversAllBranches() throws Exception {
213+
createInjector();
214+
java.lang.reflect.Method toTimestampMethod =
215+
Cve202017526Detector.class.getDeclaredMethod("toTimestamp", long.class);
216+
toTimestampMethod.setAccessible(true);
217+
218+
// Test l == 0
219+
Truth.assertThat(toTimestampMethod.invoke(detector, 0L)).isEqualTo("");
220+
221+
// Test l > 0 with leading zero byte (e.g., 2147483648L)
222+
Truth.assertThat(toTimestampMethod.invoke(detector, 2147483648L)).isEqualTo("gAAAAA");
223+
224+
// Test l < 0
225+
try {
226+
toTimestampMethod.invoke(detector, -1L);
227+
Truth.assertWithMessage("Expected InvocationTargetException").fail();
228+
} catch (java.lang.reflect.InvocationTargetException e) {
229+
Truth.assertThat(e.getCause()).isInstanceOf(IllegalArgumentException.class);
230+
}
231+
}
232+
181233
private void startMockWebServer() throws IOException {
182234
final Dispatcher dispatcher =
183235
new Dispatcher() {

0 commit comments

Comments
 (0)