Skip to content

Commit 613af34

Browse files
Merge pull request #177 from AikidoSec/create-spring-app-2.7
Add support for Spring v2
2 parents 6535639 + 5d88dfb commit 613af34

34 files changed

Lines changed: 1503 additions & 7 deletions

.github/workflows/end2end.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ jobs:
4444
- { name: SpringMVCPostgresGroovy, test_file: end2end/spring_mvc_postgres_groovy.py }
4545
- { name: JavalinPostgres, test_file: end2end/javalin_postgres.py }
4646
- { name: JavalinMySQLKotlin, test_file: end2end/javalin_mysql_kotlin.py }
47+
- { name: SpringBoot2.7Postgres, test_file: end2end/spring_boot_2.7_postgres.py }
4748
java-version: [17, 18, 19, 20, 21]
4849
distribution: ['adopt', 'corretto', 'oracle']
4950
steps:

agent/build.gradle

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ dependencies {
77
implementation project(':agent_api')
88
implementation 'net.bytebuddy:byte-buddy:1.15.11'
99
// Compile only for interface types :
10-
compileOnly 'jakarta.servlet:jakarta.servlet-api:6.1.0' // For Context object (Spring Boot)
10+
compileOnly 'jakarta.servlet:jakarta.servlet-api:6.1.0' // spring 3 -> jakarta
11+
compileOnly 'javax.servlet:javax.servlet-api:4.0.1' // spring 2 -> javax
1112
compileOnly 'io.projectreactor.netty:reactor-netty-http:1.2.1' // For Spring Webflux
1213
compileOnly 'io.javalin:javalin:6.4.0'
1314
compileOnly 'org.springframework:spring-web:5.3.20'
@@ -25,4 +26,4 @@ shadowJar {
2526
'Implementation-Version': rootProject.version
2627
)
2728
}
28-
}
29+
}

agent/src/main/java/dev/aikido/agent/Wrappers.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
import dev.aikido.agent.wrappers.file.FileConstructorSingleArgumentWrapper;
66
import dev.aikido.agent.wrappers.javalin.*;
77
import dev.aikido.agent.wrappers.jdbc.*;
8+
import dev.aikido.agent.wrappers.spring.SpringMVCJavaxWrapper;
89
import dev.aikido.agent.wrappers.spring.SpringWebfluxWrapper;
910
import dev.aikido.agent.wrappers.spring.SpringControllerWrapper;
10-
import dev.aikido.agent.wrappers.spring.SpringMVCWrapper;
11+
import dev.aikido.agent.wrappers.spring.SpringMVCJakartaWrapper;
1112

1213
import java.util.Arrays;
1314
import java.util.List;
@@ -16,7 +17,8 @@ public final class Wrappers {
1617
private Wrappers() {}
1718
public static final List<Wrapper> WRAPPERS = Arrays.asList(
1819
new PostgresWrapper(),
19-
new SpringMVCWrapper(),
20+
new SpringMVCJakartaWrapper(),
21+
new SpringMVCJavaxWrapper(),
2022
new SpringWebfluxWrapper(),
2123
new SpringControllerWrapper(),
2224
new FileConstructorSingleArgumentWrapper(),

agent/src/main/java/dev/aikido/agent/wrappers/spring/SpringMVCWrapper.java renamed to agent/src/main/java/dev/aikido/agent/wrappers/spring/SpringMVCJakartaWrapper.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@
2424
import java.util.List;
2525
import static net.bytebuddy.implementation.bytecode.assign.Assigner.Typing.DYNAMIC;
2626
import static net.bytebuddy.matcher.ElementMatchers.nameContains;
27+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
2728

28-
public class SpringMVCWrapper implements Wrapper {
29-
public static final Logger logger = LogManager.getLogger(SpringMVCWrapper.class);
29+
public class SpringMVCJakartaWrapper implements Wrapper {
30+
public static final Logger logger = LogManager.getLogger(SpringMVCJakartaWrapper.class);
3031

3132
@Override
3233
public String getName() {
@@ -38,7 +39,9 @@ public String getName() {
3839
}
3940
@Override
4041
public ElementMatcher<? super MethodDescription> getMatcher() {
41-
return ElementMatchers.nameContainsIgnoreCase("doFilterInternal");
42+
return ElementMatchers.nameContainsIgnoreCase("doFilterInternal")
43+
.and(takesArgument(0, nameContains("jakarta")))
44+
.and(takesArgument(1, nameContains("jakarta")));
4245
}
4346

4447
@Override
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package dev.aikido.agent.wrappers.spring;
2+
3+
import dev.aikido.agent.wrappers.Wrapper;
4+
import dev.aikido.agent_api.collectors.WebRequestCollector;
5+
import dev.aikido.agent_api.collectors.WebResponseCollector;
6+
import dev.aikido.agent_api.context.ContextObject;
7+
import dev.aikido.agent_api.context.SpringMVCContextObject;
8+
import dev.aikido.agent_api.helpers.logging.LogManager;
9+
import dev.aikido.agent_api.helpers.logging.Logger;
10+
11+
import javax.servlet.http.Cookie;
12+
import javax.servlet.http.HttpServletRequest;
13+
import javax.servlet.http.HttpServletResponse;
14+
import net.bytebuddy.asm.Advice;
15+
import net.bytebuddy.description.method.MethodDescription;
16+
import net.bytebuddy.description.type.TypeDescription;
17+
import net.bytebuddy.matcher.ElementMatcher;
18+
import net.bytebuddy.matcher.ElementMatchers;
19+
20+
import java.lang.reflect.Executable;
21+
import java.util.ArrayList;
22+
import java.util.Enumeration;
23+
import java.util.HashMap;
24+
import java.util.List;
25+
26+
import static net.bytebuddy.implementation.bytecode.assign.Assigner.Typing.DYNAMIC;
27+
import static net.bytebuddy.matcher.ElementMatchers.nameContains;
28+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
29+
30+
public class SpringMVCJavaxWrapper implements Wrapper {
31+
public static final Logger logger = LogManager.getLogger(SpringMVCJavaxWrapper.class);
32+
33+
@Override
34+
public String getName() {
35+
// We wrap the function doFilterInternal which gets called with
36+
// HttpServletRequest request, HttpServletResponse response
37+
// And is part of org.springframework.web.filter.RequestContextFilter
38+
// See: https://github.com/spring-projects/spring-framework/blob/4749d810db0261ce16ae5f32da6d375bb8087430/spring-web/src/main/java/org/springframework/web/filter/RequestContextFilter.java#L92
39+
return SpringMVCAdvice.class.getName();
40+
}
41+
@Override
42+
public ElementMatcher<? super MethodDescription> getMatcher() {
43+
return ElementMatchers.nameContainsIgnoreCase("doFilterInternal")
44+
.and(takesArgument(0, nameContains("javax")))
45+
.and(takesArgument(1, nameContains("javax")));
46+
}
47+
48+
@Override
49+
public ElementMatcher<? super TypeDescription> getTypeMatcher() {
50+
return nameContains("org.springframework.web.filter.RequestContextFilter");
51+
}
52+
53+
public static class SpringMVCAdvice {
54+
// Wrapper to skip if it's inside this wrapper (i.e. our own response : )
55+
public record SkipOnWrapper(HttpServletResponse response) {};
56+
/**
57+
* @return the first value of Object is used as a boolean, if it's true code will
58+
* not execute (skip execution). The second value is the servlet request.
59+
*/
60+
@Advice.OnMethodEnter(skipOn = SkipOnWrapper.class, suppress = Throwable.class)
61+
public static Object interceptOnEnter(
62+
@Advice.Origin Executable method,
63+
@Advice.Argument(value = 0, typing = DYNAMIC, optional = true) HttpServletRequest request,
64+
@Advice.Argument(value = 1, typing = DYNAMIC, optional = true) HttpServletResponse response) throws Throwable {
65+
if (request == null) {
66+
logger.debug("@OnMethodEnter: request is null, not parsing request.");
67+
return response;
68+
}
69+
// extract headers :
70+
HashMap<String, Enumeration<String>> headersMap = new HashMap<>();
71+
Enumeration<String> headerNames = request.getHeaderNames();
72+
while (headerNames != null && headerNames.hasMoreElements()) {
73+
String headerName = headerNames.nextElement();
74+
Enumeration<String> headerValue = request.getHeaders(headerName);
75+
headersMap.put(headerName, headerValue);
76+
}
77+
// extract cookies :
78+
HashMap<String, List<String>> cookiesMap = new HashMap<>();
79+
Cookie[] cookies = request.getCookies();
80+
if (cookies != null) {
81+
for (Cookie cookie : cookies) {
82+
// If no entry exists, create a new empty entry
83+
if (!cookiesMap.containsKey(cookie.getName())) {
84+
cookiesMap.put(cookie.getName(), new ArrayList<>());
85+
}
86+
cookiesMap.get(cookie.getName()).add(cookie.getValue());
87+
}
88+
}
89+
90+
ContextObject contextObject = new SpringMVCContextObject(
91+
request.getMethod(), request.getRequestURL(), request.getRemoteAddr(),
92+
request.getParameterMap(), cookiesMap, headersMap
93+
);
94+
95+
// Write a new response:
96+
WebRequestCollector.Res res = WebRequestCollector.report(contextObject);
97+
if (res != null) {
98+
logger.trace("Writing a new response");
99+
response.setStatus(res.status());
100+
response.setContentType("text/plain");
101+
response.getWriter().write(res.msg());
102+
return new SkipOnWrapper(response);
103+
}
104+
return response;
105+
}
106+
107+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
108+
public static void interceptOnExit(@Advice.Enter Object response) {
109+
if (response != null && response instanceof HttpServletResponse httpServletResponse) {
110+
WebResponseCollector.report(httpServletResponse.getStatus()); // Report status code.
111+
}
112+
}
113+
}
114+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from utils import App, Request
2+
3+
spring_boot_postgres_app = App(8104)
4+
5+
spring_boot_postgres_app.add_payload("sql",
6+
safe_request=Request("/api/pets/create", body={"name": "Bobby"}),
7+
unsafe_request=Request("/api/pets/create", body={"name": "Malicious Pet', 'Gru from the Minions') -- "})
8+
)
9+
spring_boot_postgres_app.add_payload("command injection",
10+
safe_request=Request("/api/commands/execute/Johnny", method='GET'),
11+
unsafe_request=Request("/api/commands/execute/%27%3B%20sleep%202%3B%20%23%20", method='GET'),
12+
)
13+
spring_boot_postgres_app.add_payload("server-side request forgery",
14+
safe_request=Request("/api/requests/get", data_type='form', body={"url": "https://aikido.dev/"}),
15+
unsafe_request=Request("/api/requests/get", data_type='form', body={"url": "http://localhost:5000"})
16+
)
17+
spring_boot_postgres_app.add_payload("path traversal",
18+
safe_request=Request("/api/files/read", data_type='form', body={"fileName": "README.md"}),
19+
unsafe_request=Request("/api/files/read", data_type='form', body={"fileName": "./../databases/docker-compose.yml"})
20+
)
21+
22+
spring_boot_postgres_app.test_all_payloads()

sample-apps/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Here is an overview of all of our sample apps together with their port numbers.
2222
- [SpringBoot MVC with Postgres (Kotlin)](./SpringMVCPostgresKotlin) on port [`8092`](http://localhost:8092/)
2323
- [SpringBoot MVC with Postgres (Groovy)](./SpringMVCPostgresGroovy) on port [`8094`](http://localhost:8094/)
2424
- [SpringBoot MVC With SQLite](./SpringBootSQLite) on port [`8100`](http://localhost:8100/)
25+
- [SpringBoot MVC v2.7 With Postgres](./SpringBoot2.7Postgres) on port [`8104`](http://localhost:8104/)
2526

2627
#### Webflux Apps
2728
- [SpringBoot Webflux with Postgres](./SpringWebfluxSampleApp) on port [`8090`](http://localhost:8090/)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
**output**.log
2+
dd-java-agent**
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Define variables
2+
GRADLEW = ./gradlew
3+
JAR_FILE = build/libs/demo-0.0.1-SNAPSHOT.jar
4+
JAVA_AGENT = ../../dist/agent.jar
5+
6+
# Default target
7+
.PHONY: all
8+
all: build
9+
10+
# Build the project
11+
.PHONY: build
12+
build:
13+
@echo "Building the project..."
14+
chmod +x $(GRADLEW)
15+
$(GRADLEW) build
16+
17+
# Run the application with the Java agent
18+
.PHONY: run
19+
run: build
20+
@echo "Running SpringBoot2.7Postgres with Zen & ENV (http://localhost:8104)"
21+
AIKIDO_LOG_LEVEL="error" \
22+
AIKIDO_TOKEN="token" \
23+
AIKIDO_REALTIME_ENDPOINT="http://localhost:5000/realtime" \
24+
AIKIDO_ENDPOINT="http://localhost:5000" \
25+
AIKIDO_BLOCK=1 \
26+
nohup java -javaagent:$(JAVA_AGENT) -jar $(JAR_FILE) --server.port=8104 > output1.log &
27+
28+
.PHONY: runWithDdTrace
29+
runWithDdTrace: build
30+
@echo "Running SpringBoot2.7Postgres with Zen, ddtrace & ENV (http://localhost:8104)"
31+
wget -O dd-java-agent.jar 'https://dtdg.co/latest-java-tracer'
32+
AIKIDO_LOG_LEVEL="error" \
33+
AIKIDO_TOKEN="token" \
34+
AIKIDO_REALTIME_ENDPOINT="http://localhost:5000/realtime" \
35+
AIKIDO_ENDPOINT="http://localhost:5000" \
36+
AIKIDO_BLOCK=1 \
37+
nohup java -javaagent:dd-java-agent.jar -Ddd.profiling.enabled=true -Ddd.logs.injection=true -Ddd.service=my-app -Ddd.env=staging -Ddd.version=1.0 -javaagent:$(JAVA_AGENT) -jar $(JAR_FILE) --server.port=8104 > output1.log &
38+
39+
# Run the application without Zen
40+
.PHONY: runWithoutZen
41+
runWithoutZen: build
42+
@echo "Running SpringBoot2.7Postgres without Zen & ENV (http://localhost:8105)"
43+
AIKIDO_TOKEN="random-invalid-token" \
44+
nohup java -jar $(JAR_FILE) --server.port=8105 > output2.log &
45+
46+
# Clean the project
47+
.PHONY: clean
48+
clean:
49+
@echo "Cleaning the project..."
50+
$(GRADLEW) clean
51+
52+
.PHONY: kill
53+
kill:
54+
pkill -f java
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# SpringBoot v2.7 +Postgres vulnerable sample app
2+
- Inserting a malicious dog : `Malicious Pet', 'Gru from the Minions') -- `

0 commit comments

Comments
 (0)