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
1 change: 1 addition & 0 deletions .idea/.name

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

594 changes: 565 additions & 29 deletions README.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies {
testImplementation platform('org.junit:junit-bom:6.0.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2")
}

test {
Expand Down
70 changes: 70 additions & 0 deletions rules.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
rules:
- id: javalin-xss-in-userprofile
patterns:
- pattern: |
ctx.contentType("text/html").result(...)
- pattern: |
$HTML + $USERINPUT
message: |
Potential Reflected XSS (CWE-79).
User-controlled data (userName) is concatenated directly into HTML response without escaping.
Use HTML encoding (e.g., StringEscapeUtils.escapeHtml4) or a template engine.
languages:
- java
severity: ERROR
metadata:
cwe: "CWE-79"
owasp: "A03:2021 - Injection"
confidence: HIGH

- id: javalin-path-traversal-in-export
pattern: |
new File($DIR + $FILENAME)
message: |
Potential Path Traversal (CWE-22).
User-controlled filename is concatenated with base directory without validation.
Attacker can use "../" to write files outside /tmp/reports/.
Use Paths.get(baseDir).resolve(filename).normalize() and verify the result starts with baseDir.
languages:
- java
severity: ERROR
metadata:
cwe: "CWE-22"
owasp: "A01:2021 - Broken Access Control"
confidence: HIGH

- id: javalin-ssrf-in-notify
patterns:
- pattern: |
URL $URL = new URL($USERINPUT);
...
$URL.openConnection();
message: |
Potential Server-Side Request Forgery (CWE-918).
User-controlled URL is used to make server-side HTTP requests without validation.
Attacker can access internal services (localhost, 169.254.169.254, internal IPs).
Validate URL against allowlist and block private/internal IP ranges.
languages:
- java
severity: ERROR
metadata:
cwe: "CWE-918"
owasp: "A10:2021 - Server-Side Request Forgery"
confidence: HIGH

- id: javalin-info-disclosure-in-errors
pattern-either:
- pattern: |
ctx.status(400).result("Invalid data: " + e.getMessage())
- pattern: |
ctx.status(400).result(e.getMessage())
message: |
Potential Information Disclosure (CWE-209).
Exception messages are returned directly to the client, revealing internal logic.
Return generic error messages and log detailed errors server-side.
languages:
- java
severity: WARNING
metadata:
cwe: "CWE-209"
confidence: MEDIUM
1 change: 1 addition & 0 deletions semgrep-report.sarif
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"version":"2.1.0","runs":[{"invocations":[{"executionSuccessful":true,"toolExecutionNotifications":[{"descriptor":{"id":"SemgrepError"},"level":"error","message":{"text":"Invalid scanning root: /src"}}]}],"results":[],"tool":{"driver":{"name":"Semgrep OSS","rules":[],"semanticVersion":"1.159.0"}}}],"$schema":"https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/schemas/sarif-schema-2.1.0.json"}
1 change: 1 addition & 0 deletions semgrep1-report.sarif
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"version":"2.1.0","runs":[{"invocations":[{"executionSuccessful":true,"toolExecutionNotifications":[]}],"results":[{"fingerprints":{"matchBasedId/v1":"requires login"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"src/main/java/ru/itmo/testing/lab4/controller/UserAnalyticsController.java","uriBaseId":"%SRCROOT%"},"region":{"endColumn":74,"endLine":74,"snippet":{"text":" ctx.status(400).result(\"Invalid data: \" + e.getMessage());"},"startColumn":17,"startLine":74}}}],"message":{"text":"Potential Information Disclosure (CWE-209).\nException messages are returned directly to the client, revealing internal logic.\nReturn generic error messages and log detailed errors server-side.\n"},"properties":{},"ruleId":"javalin-info-disclosure-in-errors"},{"fingerprints":{"matchBasedId/v1":"requires login"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"src/main/java/ru/itmo/testing/lab4/controller/UserAnalyticsController.java","uriBaseId":"%SRCROOT%"},"region":{"endColumn":74,"endLine":115,"snippet":{"text":" ctx.status(400).result(\"Invalid data: \" + e.getMessage());"},"startColumn":17,"startLine":115}}}],"message":{"text":"Potential Information Disclosure (CWE-209).\nException messages are returned directly to the client, revealing internal logic.\nReturn generic error messages and log detailed errors server-side.\n"},"properties":{},"ruleId":"javalin-info-disclosure-in-errors"},{"fingerprints":{"matchBasedId/v1":"requires login"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"src/main/java/ru/itmo/testing/lab4/controller/UserAnalyticsController.java","uriBaseId":"%SRCROOT%"},"region":{"endColumn":68,"endLine":154,"snippet":{"text":" File reportFile = new File(REPORTS_BASE_DIR + filename);"},"startColumn":31,"startLine":154}}}],"message":{"text":"Potential Path Traversal (CWE-22).\nUser-controlled filename is concatenated with base directory without validation.\nAttacker can use \"../\" to write files outside /tmp/reports/.\nUse Paths.get(baseDir).resolve(filename).normalize() and verify the result starts with baseDir.\n"},"properties":{},"ruleId":"javalin-path-traversal-in-export"},{"fingerprints":{"matchBasedId/v1":"requires login"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"src/main/java/ru/itmo/testing/lab4/controller/UserAnalyticsController.java","uriBaseId":"%SRCROOT%"},"region":{"endColumn":65,"endLine":181,"snippet":{"text":" URL url = new URL(callbackUrl);\n URLConnection connection = url.openConnection();"},"startColumn":17,"startLine":180}}}],"message":{"text":"Potential Server-Side Request Forgery (CWE-918).\nUser-controlled URL is used to make server-side HTTP requests without validation.\nAttacker can access internal services (localhost, 169.254.169.254, internal IPs).\nValidate URL against allowlist and block private/internal IP ranges.\n"},"properties":{},"ruleId":"javalin-ssrf-in-notify"}],"tool":{"driver":{"name":"Semgrep OSS","rules":[{"defaultConfiguration":{"level":"warning"},"fullDescription":{"text":"Potential Information Disclosure (CWE-209).\nException messages are returned directly to the client, revealing internal logic.\nReturn generic error messages and log detailed errors server-side.\n"},"help":{"markdown":"Potential Information Disclosure (CWE-209).\nException messages are returned directly to the client, revealing internal logic.\nReturn generic error messages and log detailed errors server-side.\n","text":"Potential Information Disclosure (CWE-209).\nException messages are returned directly to the client, revealing internal logic.\nReturn generic error messages and log detailed errors server-side.\n"},"id":"javalin-info-disclosure-in-errors","name":"javalin-info-disclosure-in-errors","properties":{"precision":"very-high","tags":["CWE-209","MEDIUM CONFIDENCE","security"]},"shortDescription":{"text":"Semgrep Finding: javalin-info-disclosure-in-errors"}},{"defaultConfiguration":{"level":"error"},"fullDescription":{"text":"Potential Path Traversal (CWE-22).\nUser-controlled filename is concatenated with base directory without validation.\nAttacker can use \"../\" to write files outside /tmp/reports/.\nUse Paths.get(baseDir).resolve(filename).normalize() and verify the result starts with baseDir.\n"},"help":{"markdown":"Potential Path Traversal (CWE-22).\nUser-controlled filename is concatenated with base directory without validation.\nAttacker can use \"../\" to write files outside /tmp/reports/.\nUse Paths.get(baseDir).resolve(filename).normalize() and verify the result starts with baseDir.\n","text":"Potential Path Traversal (CWE-22).\nUser-controlled filename is concatenated with base directory without validation.\nAttacker can use \"../\" to write files outside /tmp/reports/.\nUse Paths.get(baseDir).resolve(filename).normalize() and verify the result starts with baseDir.\n"},"id":"javalin-path-traversal-in-export","name":"javalin-path-traversal-in-export","properties":{"precision":"very-high","tags":["CWE-22","HIGH CONFIDENCE","OWASP-A01:2021 - Broken Access Control","security"]},"shortDescription":{"text":"Semgrep Finding: javalin-path-traversal-in-export"}},{"defaultConfiguration":{"level":"error"},"fullDescription":{"text":"Potential Server-Side Request Forgery (CWE-918).\nUser-controlled URL is used to make server-side HTTP requests without validation.\nAttacker can access internal services (localhost, 169.254.169.254, internal IPs).\nValidate URL against allowlist and block private/internal IP ranges.\n"},"help":{"markdown":"Potential Server-Side Request Forgery (CWE-918).\nUser-controlled URL is used to make server-side HTTP requests without validation.\nAttacker can access internal services (localhost, 169.254.169.254, internal IPs).\nValidate URL against allowlist and block private/internal IP ranges.\n","text":"Potential Server-Side Request Forgery (CWE-918).\nUser-controlled URL is used to make server-side HTTP requests without validation.\nAttacker can access internal services (localhost, 169.254.169.254, internal IPs).\nValidate URL against allowlist and block private/internal IP ranges.\n"},"id":"javalin-ssrf-in-notify","name":"javalin-ssrf-in-notify","properties":{"precision":"very-high","tags":["CWE-918","HIGH CONFIDENCE","OWASP-A10:2021 - Server-Side Request Forgery","security"]},"shortDescription":{"text":"Semgrep Finding: javalin-ssrf-in-notify"}},{"defaultConfiguration":{"level":"error"},"fullDescription":{"text":"Potential Reflected XSS (CWE-79).\nUser-controlled data (userName) is concatenated directly into HTML response without escaping.\nUse HTML encoding (e.g., StringEscapeUtils.escapeHtml4) or a template engine.\n"},"help":{"markdown":"Potential Reflected XSS (CWE-79).\nUser-controlled data (userName) is concatenated directly into HTML response without escaping.\nUse HTML encoding (e.g., StringEscapeUtils.escapeHtml4) or a template engine.\n","text":"Potential Reflected XSS (CWE-79).\nUser-controlled data (userName) is concatenated directly into HTML response without escaping.\nUse HTML encoding (e.g., StringEscapeUtils.escapeHtml4) or a template engine.\n"},"id":"javalin-xss-in-userprofile","name":"javalin-xss-in-userprofile","properties":{"precision":"very-high","tags":["CWE-79","HIGH CONFIDENCE","OWASP-A03:2021 - Injection","security"]},"shortDescription":{"text":"Semgrep Finding: javalin-xss-in-userprofile"}}],"semanticVersion":"1.159.0"}}}],"$schema":"https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/schemas/sarif-schema-2.1.0.json"}
71 changes: 71 additions & 0 deletions src/test/java/ru/itmo/testing/lab4/pentest/InfoDisclosureTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package ru.itmo.testing.lab4.pentest;

import io.javalin.Javalin;
import org.junit.jupiter.api.*;
import ru.itmo.testing.lab4.controller.UserAnalyticsController;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

import static org.junit.jupiter.api.Assertions.*;

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class InfoDisclosureTest {

private static final int TEST_PORT = 7780;
private static final String BASE_URL = "http://localhost:" + TEST_PORT;

private static Javalin app;
private static HttpClient http;

@BeforeAll
static void startServer() {
app = UserAnalyticsController.createApp();
app.start(TEST_PORT);
http = HttpClient.newHttpClient();
}

@AfterAll
static void stopServer() {
app.stop();
}

@Test
@Order(1)
@DisplayName("[SECURITY] Error messages disclose internal parsing details")
void errorMessagesDiscloseInternalDetails() throws Exception {
// Act: отправляем невалидный формат месяца
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/monthlyActivity?userId=test&month=2026/01"))
.GET()
.build();
HttpResponse<String> response = http.send(request, HttpResponse.BodyHandlers.ofString());

// Assert: уязвимость подтверждена — ответ содержит детали парсинга
assertEquals(400, response.statusCode());
assertTrue(response.body().contains("could not be parsed"),
"FAIL: Error message discloses internal parsing details.");
assertTrue(response.body().contains("Text '2026/01'"),
"FAIL: User input reflected in error message with parsing context.");
}

@Test
@Order(2)
@DisplayName("[SECURITY] Error message discloses business logic for non-existent user")
void errorMessageDisclosesBusinessLogic() throws Exception {
// Act: запрашиваем несуществующего пользователя
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/monthlyActivity?userId=nonexistent&month=2026-01"))
.GET()
.build();
HttpResponse<String> response = http.send(request, HttpResponse.BodyHandlers.ofString());

// Assert: уязвимость подтверждена — раскрывается информация о сессиях
assertEquals(400, response.statusCode());
assertTrue(response.body().contains("No sessions found for user"),
"FAIL: Error message discloses that user exists but has no sessions.");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package ru.itmo.testing.lab4.pentest;

import io.javalin.Javalin;
import org.junit.jupiter.api.*;
import ru.itmo.testing.lab4.controller.UserAnalyticsController;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

import static org.junit.jupiter.api.Assertions.*;

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class MissingAuthPentestTest {

private static final int TEST_PORT = 7781;
private static final String BASE_URL = "http://localhost:" + TEST_PORT;

private static Javalin app;
private static HttpClient http;

@BeforeAll
static void startServer() {
app = UserAnalyticsController.createApp();
app.start(TEST_PORT);
http = HttpClient.newHttpClient();
}

@AfterAll
static void stopServer() {
app.stop();
}

@Test
@Order(1)
@DisplayName("[SECURITY] Any user can access another user's data")
void anyUserCanAccessAnotherUsersData() throws Exception {
// Arrange: создаем двух пользователей
http.send(HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/register?userId=alice&userName=Alice"))
.POST(HttpRequest.BodyPublishers.noBody())
.build(), HttpResponse.BodyHandlers.ofString());

http.send(HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/register?userId=bob&userName=Bob"))
.POST(HttpRequest.BodyPublishers.noBody())
.build(), HttpResponse.BodyHandlers.ofString());

// Добавляем сессию Alice
http.send(HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/recordSession?userId=alice&loginTime=2026-01-15T10:00:00&logoutTime=2026-01-15T11:00:00"))
.POST(HttpRequest.BodyPublishers.noBody())
.build(), HttpResponse.BodyHandlers.ofString());

// Act: Bob (злоумышленник) запрашивает данные Alice
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/totalActivity?userId=alice"))
.GET()
.build();
HttpResponse<String> response = http.send(request, HttpResponse.BodyHandlers.ofString());

// Assert: уязвимость подтверждена — Bob получил данные Alice
assertEquals(200, response.statusCode());
assertTrue(response.body().contains("60 minutes") || response.body().contains("Total activity:"),
"FAIL: Missing authentication allows access to another user's data.");
}

@Test
@Order(2)
@DisplayName("[SECURITY] Any user can add sessions for another user")
void anyUserCanAddSessionsForAnotherUser() throws Exception {
// Act: Bob добавляет сессию для Alice
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/recordSession?userId=alice&loginTime=2026-01-15T12:00:00&logoutTime=2026-01-15T13:00:00"))
.POST(HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = http.send(request, HttpResponse.BodyHandlers.ofString());

// Assert: уязвимость подтверждена — сессия добавлена
assertEquals(200, response.statusCode());
assertTrue(response.body().contains("Session recorded"),
"FAIL: Missing authentication allows modifying another user's data.");
}

}
Loading