Skip to content

Commit 0514a0b

Browse files
committed
Merge branch 'main' into add-outbound-blocking
2 parents bf4ccd7 + bcf4c4d commit 0514a0b

14 files changed

Lines changed: 214 additions & 31 deletions

File tree

.github/workflows/release.yml

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@ jobs:
2828
- name: Download binaries & Build with Gradle
2929
working-directory: ./
3030
run: chmod +x gradlew && make binaries && make build
31-
- name: Create a zip file of the build
32-
run:
33-
mv dist/ zen/ && zip -r zen.zip zen/*
34-
- name: Generate SHA checksum
31+
- name: Create zip and tar.gz files of the build
32+
run: |
33+
mv dist/ zen/
34+
zip -r zen.zip zen/*
35+
tar -czf zen.tar.gz zen/*
36+
- name: Generate SHA checksums
3537
run: |
3638
shasum -a 256 zen.zip > zen.zip.sha256sum
37-
39+
shasum -a 256 zen.tar.gz > zen.tar.gz.sha256sum
3840
- name: Upload Release Asset
3941
uses: actions/upload-release-asset@v1
4042
env:
@@ -53,3 +55,21 @@ jobs:
5355
asset_path: ./zen.zip.sha256sum
5456
asset_name: zen.zip.sha256sum
5557
asset_content_type: text/plain
58+
- name: Upload Release Asset (tar.gz)
59+
uses: actions/upload-release-asset@v1
60+
env:
61+
GITHUB_TOKEN: ${{ github.token }}
62+
with:
63+
upload_url: ${{ github.event.release.upload_url }}
64+
asset_path: ./zen.tar.gz
65+
asset_name: zen.tar.gz
66+
asset_content_type: application/gzip
67+
- name: Upload Release Asset (tar.gz SHA)
68+
uses: actions/upload-release-asset@v1
69+
env:
70+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
71+
with:
72+
upload_url: ${{ github.event.release.upload_url }}
73+
asset_path: ./zen.tar.gz.sha256sum
74+
asset_name: zen.tar.gz.sha256sum
75+
asset_content_type: text/plain

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ cov: check_binaries
3232

3333
# Binaries :
3434

35-
BASE_URL = https://github.com/AikidoSec/zen-internals/releases/download/v0.1.43
35+
BASE_URL = https://github.com/AikidoSec/zen-internals/releases/download/v0.1.60
3636
FILES = \
3737
libzen_internals_aarch64-apple-darwin.dylib \
3838
libzen_internals_aarch64-apple-darwin.dylib.sha256sum \

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,10 @@ Zen operates autonomously on the same server as your Java app to:
6868
You can install Zen by downloading the zip file [here](https://github.com/AikidoSec/firewall-java/releases/latest) and unzipping it to your directory of choice,
6969
we picked `/opt/zen` here for our Linux system as an example. We also have support for Windows and Mac OS X.
7070
Please ensure you do not alter the structure of this folder and that the process can read & write to it.
71-
To activate Zen you then just have to add the following argument to your Java command :
71+
72+
To activate Zen you then just have to add the following `-javaagent` to your Java command **before** the `-jar` argument
7273
```
73-
-javaagent:/opt/zen/agent.jar
74+
java -javaagent:/opt/zen/agent.jar -jar build/myapp.jar
7475
```
7576
Replace `/opt/zen` with your directory of choice.
7677

agent_api/src/main/java/dev/aikido/agent_api/background/cloud/api/events/DetectedAttackWave.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
package dev.aikido.agent_api.background.cloud.api.events;
22

3+
import com.google.gson.Gson;
34
import dev.aikido.agent_api.background.cloud.GetManagerInfo;
45
import dev.aikido.agent_api.context.ContextObject;
56
import dev.aikido.agent_api.context.User;
7+
import dev.aikido.agent_api.storage.attack_wave_detector.AttackWaveDetector;
8+
import dev.aikido.agent_api.storage.attack_wave_detector.AttackWaveDetectorStore;
69

10+
import java.util.List;
711
import java.util.Map;
812

913
import static dev.aikido.agent_api.background.cloud.GetManagerInfo.getManagerInfo;
1014
import static dev.aikido.agent_api.helpers.UnixTimeMS.getUnixTimeMS;
11-
import static dev.aikido.agent_api.storage.ServiceConfigStore.getConfig;
1215

1316
public final class DetectedAttackWave {
1417
private DetectedAttackWave() {
@@ -42,9 +45,15 @@ public static DetectedAttackWaveEvent createAPIEvent(ContextObject context) {
4245
context.getHeader("user-agent"), // userAgent
4346
context.getSource() // source
4447
);
48+
49+
String ip = context.getRemoteAddress();
50+
List<AttackWaveDetector.Sample> samples = AttackWaveDetectorStore.getSamplesForIp(ip);
51+
Map<String, String> metadata = Map.of(
52+
"samples", new Gson().toJson(samples)
53+
);
54+
4555
AttackWaveData attackData = new AttackWaveData(
46-
Map.of(),
47-
context.getUser()
56+
metadata, context.getUser()
4857
);
4958

5059
return new DetectedAttackWaveEvent(

agent_api/src/main/java/dev/aikido/agent_api/storage/attack_wave_detector/AttackWaveDetector.java

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,26 @@
33
import dev.aikido.agent_api.context.ContextObject;
44
import dev.aikido.agent_api.ratelimiting.LRUCache;
55

6+
import java.util.ArrayList;
7+
import java.util.List;
8+
import java.util.Objects;
9+
610
import static dev.aikido.agent_api.vulnerabilities.attack_wave_detection.WebScanDetector.isWebScanner;
711

812
public class AttackWaveDetector {
9-
private final LRUCache<String, Integer> suspiciousRequestsMap;
13+
private final LRUCache<String, Integer> suspiciousRequestsCounts;
14+
private final LRUCache<String, List<Sample>> suspiciousRequestsSamples;
1015
private final LRUCache<String, Long> sentEventsMap;
1116
private final int attackWaveThreshold;
17+
private final int maxSamplesPerIP;
1218

1319
public AttackWaveDetector() {
1420
this(
1521
/* attackWaveThreshold, default: 15 requests */ 15,
1622
/* attackWaveTimeFrame, default: 1 min */ 60_000L,
1723
/* minTimeBetweenEvents, default: 20 min */ 20 * 60_000L,
18-
/* maxLRUEntries, default: 10,000 entries */ 10_000
24+
/* maxLRUEntries, default: 10,000 entries */ 10_000,
25+
/* maxSamplesPerIP, default: 15 samples */ 15
1926
);
2027
}
2128

@@ -26,9 +33,11 @@ public AttackWaveDetector() {
2633
* @param maxLRUEntries Maximum number of entries in the LRU cache
2734
*/
2835
public AttackWaveDetector(int attackWaveThreshold, long attackWaveTimeFrame,
29-
long minTimeBetweenEvents, int maxLRUEntries) {
36+
long minTimeBetweenEvents, int maxLRUEntries, int maxSamplesPerIP) {
3037
this.attackWaveThreshold = attackWaveThreshold;
31-
this.suspiciousRequestsMap = new LRUCache<>(maxLRUEntries, attackWaveTimeFrame);
38+
this.maxSamplesPerIP = maxSamplesPerIP;
39+
this.suspiciousRequestsCounts = new LRUCache<>(maxLRUEntries, attackWaveTimeFrame);
40+
this.suspiciousRequestsSamples = new LRUCache<>(maxLRUEntries, attackWaveTimeFrame);
3241
this.sentEventsMap = new LRUCache<>(maxLRUEntries, minTimeBetweenEvents);
3342
}
3443

@@ -56,16 +65,42 @@ public boolean check(ContextObject ctx) {
5665

5766
// Add 1 to the suspiciousRequests counter.
5867
int suspiciousRequests = 1;
59-
Integer existingSuspiciousRequests = this.suspiciousRequestsMap.get(ip);
68+
Integer existingSuspiciousRequests = this.suspiciousRequestsCounts.get(ip);
6069
if (existingSuspiciousRequests != null) {
6170
suspiciousRequests = existingSuspiciousRequests + 1;
6271
}
63-
this.suspiciousRequestsMap.set(ip, suspiciousRequests);
72+
this.suspiciousRequestsCounts.set(ip, suspiciousRequests);
73+
74+
this.trackSample(ip, ctx.getMethod(), ctx.getUrl());
6475

6576
if (suspiciousRequests < this.attackWaveThreshold) {
6677
return false;
6778
}
6879
this.sentEventsMap.set(ip, System.currentTimeMillis());
6980
return true;
7081
}
82+
83+
public List<Sample> getSamplesForIp(String ip) {
84+
List<Sample> samples = this.suspiciousRequestsSamples.get(ip);
85+
if (samples == null) {
86+
samples = new ArrayList<>();
87+
}
88+
return samples;
89+
}
90+
91+
public record Sample(String method, String url) {}
92+
private void trackSample(String ip, String method, String url) {
93+
List<Sample> samples = getSamplesForIp(ip);
94+
if (samples.size() >= this.maxSamplesPerIP) {
95+
return;
96+
}
97+
98+
for (Sample sample : samples) {
99+
if (Objects.equals(sample.method, method) && Objects.equals(sample.url, url)) {
100+
return; // an equivalent entry already exists, skipping
101+
}
102+
}
103+
samples.add(new Sample(method, url));
104+
this.suspiciousRequestsSamples.set(ip, samples);
105+
}
71106
}

agent_api/src/main/java/dev/aikido/agent_api/storage/attack_wave_detector/AttackWaveDetectorStore.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import dev.aikido.agent_api.helpers.logging.LogManager;
55
import dev.aikido.agent_api.helpers.logging.Logger;
66

7+
import java.util.List;
78
import java.util.concurrent.locks.ReentrantLock;
89

910
public final class AttackWaveDetectorStore {
@@ -25,4 +26,13 @@ public static boolean check(ContextObject ctx) {
2526
mutex.unlock();
2627
}
2728
}
29+
30+
public static List<AttackWaveDetector.Sample> getSamplesForIp(String ip) {
31+
mutex.lock();
32+
try {
33+
return detector.getSamplesForIp(ip);
34+
} finally {
35+
mutex.unlock();
36+
}
37+
}
2838
}

agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/path_traversal/UnsafePathChecker.java

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
import java.util.ArrayList;
44
import java.util.Arrays;
55
import java.util.List;
6+
import java.util.regex.Pattern;
67

78
public final class UnsafePathChecker {
89
private UnsafePathChecker() {}
10+
11+
private static final Pattern CURRENT_DIR_PATTERN = Pattern.compile("/(\\./)+");
12+
913
private static final List<String> LINUX_ROOT_FOLDERS = Arrays.asList(
1014
"/bin/",
1115
"/boot/",
@@ -25,15 +29,17 @@ private UnsafePathChecker() {}
2529
"/sys/",
2630
"/tmp/",
2731
"/usr/",
28-
"/var/"
32+
"/var/",
33+
"/app/",
34+
"/code/"
2935
);
3036
private static final List<String> DANGEROUS_PATH_STARTS = Arrays.asList(
3137
"c:/",
3238
"c:\\"
3339
);
3440

3541
public static boolean startsWithUnsafePath(String filePathRaw) {
36-
String filePath = ensureOneLeadingSlash(filePathRaw.toLowerCase());
42+
String filePath = normalizePath(filePathRaw.toLowerCase());
3743

3844
List<String> dangerousStartsList = new ArrayList<>(DANGEROUS_PATH_STARTS);
3945
dangerousStartsList.addAll(LINUX_ROOT_FOLDERS);
@@ -47,15 +53,24 @@ public static boolean startsWithUnsafePath(String filePathRaw) {
4753
}
4854

4955
public static boolean startsWithUnsafePath(String filePathRaw, String userInputRaw) {
50-
String filePath = ensureOneLeadingSlash(filePathRaw.toLowerCase());
51-
String userInput = ensureOneLeadingSlash(userInputRaw.toLowerCase());
56+
String filePath = normalizePath(filePathRaw.toLowerCase());
57+
String userInput = normalizePath(userInputRaw.toLowerCase());
5258
return startsWithUnsafePath(filePath) && filePath.startsWith(userInput);
5359
}
5460

55-
private static String ensureOneLeadingSlash(String path) {
56-
if (path.startsWith("/")) {
57-
return "/" + path.replaceAll("^/+", "");
61+
/**
62+
* Normalizes a path by removing /./ and removing consecutive slashes
63+
*/
64+
private static String normalizePath(String path) {
65+
if (path == null || path.isEmpty()) {
66+
return path;
5867
}
59-
return path;
68+
69+
// Matches /./ or /././ or /./././ etc. (one or more ./ sequences after a /)
70+
String normalized = CURRENT_DIR_PATTERN.matcher(path).replaceAll("/");
71+
72+
// Merge consecutive slashes since these don't change where you are in the path.
73+
normalized = normalized.replaceAll("/+", "/");
74+
return normalized;
6075
}
6176
}

agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/shell_injection/DangerousShellChars.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ public final class DangerousShellChars {
77
private DangerousShellChars() {}
88
private static final List<String> DANGEROUS_CHARS = Arrays.asList(
99
"#", "!", "\"", "$", "&", "'", "(", ")", "*", ";", "<", "=", ">", "?",
10-
"[", "\\", "]", "^", "`", "{", "|", "}", " ", "\n", "\t", "~"
10+
"[", "\\", "]", "^", "`", "{", "|", "}", " ", "\n", "\t", "~", "\r", "\f"
1111
);
1212

1313
public static boolean containDangerousCharacter(String userInput) {

agent_api/src/main/java/dev/aikido/agent_api/vulnerabilities/shell_injection/ShellSyntaxChecker.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
public final class ShellSyntaxChecker {
1111
private ShellSyntaxChecker() {}
1212
private static final List<String> SEPARATORS = Arrays.asList(
13-
" ", "\t", "\n", ";", "&", "|", "(", ")", "<", ">"
13+
" ", "\t", "\n", ";", "&", "|", "(", ")", "<", ">", "\r", "\f"
1414
);
1515

1616
public static boolean containsShellSyntax(String command, String userInput) {

agent_api/src/test/java/attack_wave_detection/AttackWaveDetectorTest.java

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44
import org.junit.jupiter.api.Test;
55
import utils.EmptySampleContextObject;
66

7-
import static org.junit.jupiter.api.Assertions.assertFalse;
8-
import static org.junit.jupiter.api.Assertions.assertTrue;
7+
import java.util.List;
8+
9+
import static org.junit.jupiter.api.Assertions.*;
910

1011
class AttackWaveDetectorTest {
1112

1213
private AttackWaveDetector newAttackWaveDetector() {
1314
// Use much smaller time frames for testing (e.g., 100ms instead of 60s)
14-
return new AttackWaveDetector(6, 100L, 200L, 10_000);
15+
return new AttackWaveDetector(6, 100L, 200L, 10_000, 3);
1516
}
1617

1718
private static boolean checkDetector(AttackWaveDetector detector, String ip, boolean isWebScanner) {
@@ -59,6 +60,12 @@ void testWebScannerWithDelays() throws InterruptedException {
5960
assertFalse(checkDetector(detector, "::1", true));
6061
assertFalse(checkDetector(detector, "::1", true));
6162
assertFalse(checkDetector(detector, "::1", true));
63+
assertArrayEquals(
64+
List.of(
65+
new AttackWaveDetector.Sample("BADMETHOD", "https://example.com/wp-config.php")
66+
).toArray(),
67+
detector.getSamplesForIp("::1").toArray()
68+
);
6269

6370
// Small delay (50ms)
6471
Thread.sleep(50);
@@ -118,4 +125,24 @@ void testSlowWebScannerThirdInterval() throws InterruptedException {
118125
assertFalse(checkDetector(detector, "::1", true));
119126
assertTrue(checkDetector(detector, "::1", true));
120127
}
128+
129+
@Test
130+
void testItRespectsSamplesLimit() {
131+
AttackWaveDetector detector = newAttackWaveDetector();
132+
detector.check(new EmptySampleContextObject("", "/../etc/passwd", "GET"));
133+
detector.check(new EmptySampleContextObject("", "/../etc/passwd", "GET"));
134+
detector.check(new EmptySampleContextObject("", "/test2", "GET"));
135+
detector.check(new EmptySampleContextObject("", "/../etc/passwd", "POST"));
136+
detector.check(new EmptySampleContextObject("", "/test3", "PUT"));
137+
detector.check(new EmptySampleContextObject("", "/.env", "GET"));
138+
detector.check(new EmptySampleContextObject("", "/test4", "BADMETHOD"));
139+
assertArrayEquals(
140+
List.of(
141+
new AttackWaveDetector.Sample("GET", "https://example.com/../etc/passwd"),
142+
new AttackWaveDetector.Sample("POST", "https://example.com/../etc/passwd"),
143+
new AttackWaveDetector.Sample("GET", "https://example.com/.env")
144+
).toArray(),
145+
detector.getSamplesForIp("192.168.1.1").toArray()
146+
);
147+
}
121148
}

0 commit comments

Comments
 (0)