From bfe68fb83e9271d2d1fef3549ce6ad06c315321a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 29 Jul 2025 20:32:37 +0000
Subject: [PATCH 01/17] Initial plan
From 7a4d27435f7c95640ad201c6f4a0d66bcdacb679 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 29 Jul 2025 20:51:56 +0000
Subject: [PATCH 02/17] Implement Challenge 57: Database Connection String
Exposure through Error Messages
Co-authored-by: commjoen <1457214+commjoen@users.noreply.github.com>
---
README.md | 6 +-
.../challenges/docker/Challenge57.java | 57 +++++++++++++++++++
.../docker/Challenge57Controller.java | 25 ++++++++
.../resources/explanations/challenge57.adoc | 33 +++++++++++
.../explanations/challenge57_hint.adoc | 1 +
.../explanations/challenge57_reason.adoc | 43 ++++++++++++++
.../wrong-secrets-configuration.yaml | 13 +++++
.../docker/Challenge57ControllerTest.java | 31 ++++++++++
.../challenges/docker/Challenge57Test.java | 47 +++++++++++++++
9 files changed, 253 insertions(+), 3 deletions(-)
create mode 100644 src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge57.java
create mode 100644 src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge57Controller.java
create mode 100644 src/main/resources/explanations/challenge57.adoc
create mode 100644 src/main/resources/explanations/challenge57_hint.adoc
create mode 100644 src/main/resources/explanations/challenge57_reason.adoc
create mode 100644 src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge57ControllerTest.java
create mode 100644 src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge57Test.java
diff --git a/README.md b/README.md
index b2b2067db..ae01ddba9 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@
Welcome to the OWASP WrongSecrets game! The game is packed with real life examples of how to _not_ store secrets in your software. Each of these examples is captured in a challenge, which you need to solve using various tools and techniques. Solving these challenges will help you recognize common mistakes & can help you to reflect on your own secrets management strategy.
-Can you solve all the 56 challenges?
+Can you solve all the 57 challenges?
Try some of them on [our Heroku demo environment](https://wrongsecrets.herokuapp.com/).
@@ -130,7 +130,7 @@ Not sure which setup is right for you? Here's a quick guide:
| Try it quickly online | [Container running on Heroku](https://www.wrongsecrets.com/) | Basic challenges (1-4, 8, 12-32, 34-43, 49-52, 54-55) |
| Run locally with Docker | [Basic Docker](#basic-docker-exercises) | Same as above, but on your machine |
| Learn Kubernetes secrets | [K8s/Minikube Setup](#basic-k8s-exercise) | Kubernetes challenges (1-6, 8, 12-43, 48-55) |
-| Practice with cloud secrets | [Cloud Challenges](#cloud-challenges) | All challenges (1-55) |
+| Practice with cloud secrets | [Cloud Challenges](#cloud-challenges) | All challenges (1-57) |
| Run a workshop/CTF | [CTF Setup](#ctf) | Customizable challenge sets |
| Contribute to the project | [Development Setup](#notes-on-development) | All challenges + development tools |
@@ -330,7 +330,7 @@ This is because if you run the start script again it will replace the secret in
## Cloud Challenges
-_Can be used for challenges 1-55_
+_Can be used for challenges 1-57_
**READ THIS**: Given that the exercises below contain IAM privilege escalation exercises,
never run this on an account which is related to your production environment or can influence your account-over-arching
diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge57.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge57.java
new file mode 100644
index 000000000..1064cf1f1
--- /dev/null
+++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge57.java
@@ -0,0 +1,57 @@
+package org.owasp.wrongsecrets.challenges.docker;
+
+import com.google.common.base.Strings;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import lombok.extern.slf4j.Slf4j;
+import org.owasp.wrongsecrets.challenges.Challenge;
+import org.owasp.wrongsecrets.challenges.Spoiler;
+import org.springframework.stereotype.Component;
+
+/** Challenge demonstrating database connection string exposure through error messages. */
+@Slf4j
+@Component
+public class Challenge57 implements Challenge {
+
+ // Simulated database connection string with embedded credentials
+ private static final String DB_CONNECTION_STRING =
+ "jdbc:postgresql://db.example.com:5432/userdb?user=dbadmin&password=SuperSecretDB2024!&ssl=true";
+
+ private static final String EXPECTED_SECRET = "SuperSecretDB2024!";
+
+ @Override
+ public Spoiler spoiler() {
+ return new Spoiler(EXPECTED_SECRET);
+ }
+
+ @Override
+ public boolean answerCorrect(String answer) {
+ return !Strings.isNullOrEmpty(answer) && EXPECTED_SECRET.equals(answer.trim());
+ }
+
+ /**
+ * This method simulates what happens when an application tries to connect to a database
+ * but fails, exposing the full connection string (including credentials) in error messages.
+ * This is a common real-world mistake where developers include sensitive information
+ * in connection strings and don't properly handle/sanitize database connection errors.
+ */
+ public String simulateDatabaseConnectionError() {
+ try {
+ // This will fail since we don't have a real database, but it demonstrates
+ // how connection errors can expose credentials
+ Connection conn = DriverManager.getConnection(DB_CONNECTION_STRING);
+ conn.close();
+ return "Connection successful";
+ } catch (SQLException e) {
+ // Poor error handling - exposing the full connection string in the error message
+ String errorMessage = "Database connection failed with connection string: " + DB_CONNECTION_STRING
+ + "\nError: " + e.getMessage();
+
+ // Log the error (another way credentials get exposed)
+ log.error("Failed to connect to database: {}", errorMessage);
+
+ return errorMessage;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge57Controller.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge57Controller.java
new file mode 100644
index 000000000..072aa2e66
--- /dev/null
+++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge57Controller.java
@@ -0,0 +1,25 @@
+package org.owasp.wrongsecrets.challenges.docker;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/** REST controller for Challenge 57 to trigger database connection error. */
+@Slf4j
+@RestController
+@RequiredArgsConstructor
+public class Challenge57Controller {
+
+ private final Challenge57 challenge;
+
+ /**
+ * Endpoint to trigger a database connection error that exposes connection string with credentials.
+ * This simulates what happens when applications try to connect to unavailable databases.
+ */
+ @GetMapping("/error-demo/database-connection")
+ public String triggerDatabaseError() {
+ log.info("Attempting database connection for Challenge 57...");
+ return challenge.simulateDatabaseConnectionError();
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/explanations/challenge57.adoc b/src/main/resources/explanations/challenge57.adoc
new file mode 100644
index 000000000..2957c96c3
--- /dev/null
+++ b/src/main/resources/explanations/challenge57.adoc
@@ -0,0 +1,33 @@
+=== Database Connection String Exposure in Error Messages
+
+One of the most common and dangerous ways secrets leak in real-world applications is through database connection strings that contain embedded credentials. When applications fail to connect to databases, they often expose the full connection string (including usernames and passwords) in error messages, logs, or even user-facing interfaces.
+
+This challenge demonstrates a scenario where a developer:
+
+1. **Uses embedded credentials in connection strings** instead of external secret management
+2. **Has poor error handling** that exposes the full connection string when database connections fail
+3. **Logs sensitive information** without sanitizing credentials first
+4. **Displays technical details** that could reach monitoring systems, error tracking tools, or even end users
+
+**Common places where these exposed connection strings appear:**
+
+- Application startup logs when database is unavailable
+- Exception stack traces in monitoring tools like Sentry, Rollbar, or CloudWatch
+- Error messages displayed to users during maintenance windows
+- CI/CD pipeline logs when deployment health checks fail
+- Docker container logs during orchestration failures
+
+**Real-world examples:**
+
+- Applications that fail health checks during Kubernetes deployments
+- Microservices that can't reach their database during startup
+- Database migration scripts that fail with exposed connection details
+- Development/testing environments where error verbosity is set too high
+
+**How to trigger the error:**
+
+Visit the `/error-demo/database-connection` endpoint to simulate a database connection failure. This endpoint attempts to connect to a database using a connection string with embedded credentials, and when it fails, it exposes the credentials in both the HTTP response and application logs.
+
+Can you find the database password that gets exposed when the application tries to connect to the database?
+
+**Hint:** Look for database connection error messages that reveal more than they should.
\ No newline at end of file
diff --git a/src/main/resources/explanations/challenge57_hint.adoc b/src/main/resources/explanations/challenge57_hint.adoc
new file mode 100644
index 000000000..64b37a7e0
--- /dev/null
+++ b/src/main/resources/explanations/challenge57_hint.adoc
@@ -0,0 +1 @@
+Try visiting the `/error-demo/database-connection` endpoint to trigger a database connection error. Look at both the HTTP response and the application logs - database connection failures often expose connection strings with embedded credentials in error messages.
\ No newline at end of file
diff --git a/src/main/resources/explanations/challenge57_reason.adoc b/src/main/resources/explanations/challenge57_reason.adoc
new file mode 100644
index 000000000..e15fc2a31
--- /dev/null
+++ b/src/main/resources/explanations/challenge57_reason.adoc
@@ -0,0 +1,43 @@
+=== What went wrong?
+
+The application demonstrates several critical security mistakes commonly found in real-world applications:
+
+**1. Embedded Credentials in Connection Strings**
+The database connection string contains the username and password directly embedded:
+```
+jdbc:postgresql://db.example.com:5432/userdb?user=dbadmin&password=SuperSecretDB2024!&ssl=true
+```
+
+**2. Poor Error Handling**
+When the database connection fails, the application exposes the entire connection string in the error message, revealing the embedded credentials to anyone who can see the error.
+
+**3. Sensitive Information in Logs**
+The error gets logged with the full connection string, meaning the credentials are now stored in log files that may be:
+- Accessible to multiple team members
+- Shipped to centralized logging systems
+- Stored long-term in log archives
+- Exposed through log monitoring tools
+
+**4. No Secret Sanitization**
+The application doesn't sanitize sensitive information before logging or displaying errors.
+
+**How to fix this:**
+
+1. **Use external secret management:** Store database credentials in environment variables, secret management systems (AWS Secrets Manager, Azure Key Vault, HashiCorp Vault), or configuration files not checked into version control.
+
+2. **Separate connection parameters:** Keep the connection URL separate from credentials:
+ ```
+ DB_HOST=db.example.com
+ DB_PORT=5432
+ DB_NAME=userdb
+ DB_USER=dbadmin
+ DB_PASS=SuperSecretDB2024!
+ ```
+
+3. **Implement proper error handling:** Sanitize error messages before logging or displaying them. Never include connection strings or other sensitive data in error messages.
+
+4. **Use connection pooling with secure configuration:** Modern frameworks like Spring Boot support secure configuration of connection pools without exposing credentials.
+
+5. **Monitor and audit:** Regularly review logs and error messages to ensure sensitive information isn't being inadvertently exposed.
+
+This type of credential exposure is extremely common in production applications and represents one of the most frequent ways database credentials are accidentally leaked.
\ No newline at end of file
diff --git a/src/main/resources/wrong-secrets-configuration.yaml b/src/main/resources/wrong-secrets-configuration.yaml
index d83e16f8f..0bc8a3097 100644
--- a/src/main/resources/wrong-secrets-configuration.yaml
+++ b/src/main/resources/wrong-secrets-configuration.yaml
@@ -879,3 +879,16 @@ configurations:
category: *ai
ctf:
enabled: true
+
+ - name: Challenge 57
+ short-name: "challenge-57"
+ sources:
+ - class-name: "org.owasp.wrongsecrets.challenges.docker.Challenge57"
+ explanation: "explanations/challenge57.adoc"
+ hint: "explanations/challenge57_hint.adoc"
+ reason: "explanations/challenge57_reason.adoc"
+ environments: *all_envs
+ difficulty: *normal
+ category: *logging
+ ctf:
+ enabled: true
diff --git a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge57ControllerTest.java b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge57ControllerTest.java
new file mode 100644
index 000000000..f91d4bff1
--- /dev/null
+++ b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge57ControllerTest.java
@@ -0,0 +1,31 @@
+package org.owasp.wrongsecrets.challenges.docker;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class Challenge57ControllerTest {
+
+ @Mock private Challenge57 challenge;
+
+ @InjectMocks private Challenge57Controller controller;
+
+ @Test
+ void triggerDatabaseErrorShouldReturnErrorMessage() {
+ // Given
+ String expectedError = "Database connection failed with connection string: ...";
+ org.mockito.Mockito.when(challenge.simulateDatabaseConnectionError()).thenReturn(expectedError);
+
+ // When
+ String result = controller.triggerDatabaseError();
+
+ // Then
+ assertThat(result).isEqualTo(expectedError);
+ org.mockito.Mockito.verify(challenge).simulateDatabaseConnectionError();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge57Test.java b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge57Test.java
new file mode 100644
index 000000000..8c80ad0b2
--- /dev/null
+++ b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge57Test.java
@@ -0,0 +1,47 @@
+package org.owasp.wrongsecrets.challenges.docker;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+import org.owasp.wrongsecrets.challenges.Spoiler;
+
+class Challenge57Test {
+
+ @Test
+ void spoilerShouldReturnCorrectAnswer() {
+ var challenge = new Challenge57();
+ assertThat(challenge.spoiler()).isEqualTo(new Spoiler("SuperSecretDB2024!"));
+ }
+
+ @Test
+ void answerCorrectShouldReturnTrueForCorrectAnswer() {
+ var challenge = new Challenge57();
+ assertThat(challenge.answerCorrect("SuperSecretDB2024!")).isTrue();
+ }
+
+ @Test
+ void answerCorrectShouldReturnFalseForIncorrectAnswer() {
+ var challenge = new Challenge57();
+ assertThat(challenge.answerCorrect("wronganswer")).isFalse();
+ assertThat(challenge.answerCorrect("")).isFalse();
+ assertThat(challenge.answerCorrect(null)).isFalse();
+ }
+
+ @Test
+ void answerCorrectShouldTrimWhitespace() {
+ var challenge = new Challenge57();
+ assertThat(challenge.answerCorrect(" SuperSecretDB2024! ")).isTrue();
+ }
+
+ @Test
+ void simulateDatabaseConnectionErrorShouldExposeConnectionString() {
+ var challenge = new Challenge57();
+ String errorMessage = challenge.simulateDatabaseConnectionError();
+
+ // Verify that the error message contains the exposed connection string
+ assertThat(errorMessage).contains("jdbc:postgresql://db.example.com:5432/userdb");
+ assertThat(errorMessage).contains("user=dbadmin");
+ assertThat(errorMessage).contains("password=SuperSecretDB2024!");
+ assertThat(errorMessage).contains("Database connection failed");
+ }
+}
\ No newline at end of file
From 372f123c07387180e7e5c64ede8901d91c1bbd30 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 29 Jul 2025 21:22:03 +0000
Subject: [PATCH 03/17] Fix formatting issues in Challenge 57 files
Co-authored-by: commjoen <1457214+commjoen@users.noreply.github.com>
---
.../challenges/docker/Challenge57.java | 27 ++++++++++---------
.../docker/Challenge57Controller.java | 7 ++---
.../resources/explanations/challenge57.adoc | 2 +-
.../explanations/challenge57_hint.adoc | 2 +-
.../explanations/challenge57_reason.adoc | 2 +-
.../docker/Challenge57ControllerTest.java | 2 +-
.../challenges/docker/Challenge57Test.java | 4 +--
7 files changed, 25 insertions(+), 21 deletions(-)
diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge57.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge57.java
index 1064cf1f1..a585f688b 100644
--- a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge57.java
+++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge57.java
@@ -15,9 +15,9 @@
public class Challenge57 implements Challenge {
// Simulated database connection string with embedded credentials
- private static final String DB_CONNECTION_STRING =
+ private static final String DB_CONNECTION_STRING =
"jdbc:postgresql://db.example.com:5432/userdb?user=dbadmin&password=SuperSecretDB2024!&ssl=true";
-
+
private static final String EXPECTED_SECRET = "SuperSecretDB2024!";
@Override
@@ -31,27 +31,30 @@ public boolean answerCorrect(String answer) {
}
/**
- * This method simulates what happens when an application tries to connect to a database
- * but fails, exposing the full connection string (including credentials) in error messages.
- * This is a common real-world mistake where developers include sensitive information
- * in connection strings and don't properly handle/sanitize database connection errors.
+ * This method simulates what happens when an application tries to connect to a database but
+ * fails, exposing the full connection string (including credentials) in error messages. This is a
+ * common real-world mistake where developers include sensitive information in connection strings
+ * and don't properly handle/sanitize database connection errors.
*/
public String simulateDatabaseConnectionError() {
try {
- // This will fail since we don't have a real database, but it demonstrates
+ // This will fail since we don't have a real database, but it demonstrates
// how connection errors can expose credentials
Connection conn = DriverManager.getConnection(DB_CONNECTION_STRING);
conn.close();
return "Connection successful";
} catch (SQLException e) {
// Poor error handling - exposing the full connection string in the error message
- String errorMessage = "Database connection failed with connection string: " + DB_CONNECTION_STRING
- + "\nError: " + e.getMessage();
-
+ String errorMessage =
+ "Database connection failed with connection string: "
+ + DB_CONNECTION_STRING
+ + "\nError: "
+ + e.getMessage();
+
// Log the error (another way credentials get exposed)
log.error("Failed to connect to database: {}", errorMessage);
-
+
return errorMessage;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge57Controller.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge57Controller.java
index 072aa2e66..7926bac4c 100644
--- a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge57Controller.java
+++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge57Controller.java
@@ -14,12 +14,13 @@ public class Challenge57Controller {
private final Challenge57 challenge;
/**
- * Endpoint to trigger a database connection error that exposes connection string with credentials.
- * This simulates what happens when applications try to connect to unavailable databases.
+ * Endpoint to trigger a database connection error that exposes connection string with
+ * credentials. This simulates what happens when applications try to connect to unavailable
+ * databases.
*/
@GetMapping("/error-demo/database-connection")
public String triggerDatabaseError() {
log.info("Attempting database connection for Challenge 57...");
return challenge.simulateDatabaseConnectionError();
}
-}
\ No newline at end of file
+}
diff --git a/src/main/resources/explanations/challenge57.adoc b/src/main/resources/explanations/challenge57.adoc
index 2957c96c3..378fd5a45 100644
--- a/src/main/resources/explanations/challenge57.adoc
+++ b/src/main/resources/explanations/challenge57.adoc
@@ -30,4 +30,4 @@ Visit the `/error-demo/database-connection` endpoint to simulate a database conn
Can you find the database password that gets exposed when the application tries to connect to the database?
-**Hint:** Look for database connection error messages that reveal more than they should.
\ No newline at end of file
+**Hint:** Look for database connection error messages that reveal more than they should.
diff --git a/src/main/resources/explanations/challenge57_hint.adoc b/src/main/resources/explanations/challenge57_hint.adoc
index 64b37a7e0..fef53235a 100644
--- a/src/main/resources/explanations/challenge57_hint.adoc
+++ b/src/main/resources/explanations/challenge57_hint.adoc
@@ -1 +1 @@
-Try visiting the `/error-demo/database-connection` endpoint to trigger a database connection error. Look at both the HTTP response and the application logs - database connection failures often expose connection strings with embedded credentials in error messages.
\ No newline at end of file
+Try visiting the `/error-demo/database-connection` endpoint to trigger a database connection error. Look at both the HTTP response and the application logs - database connection failures often expose connection strings with embedded credentials in error messages.
diff --git a/src/main/resources/explanations/challenge57_reason.adoc b/src/main/resources/explanations/challenge57_reason.adoc
index e15fc2a31..6d6aa51df 100644
--- a/src/main/resources/explanations/challenge57_reason.adoc
+++ b/src/main/resources/explanations/challenge57_reason.adoc
@@ -40,4 +40,4 @@ The application doesn't sanitize sensitive information before logging or display
5. **Monitor and audit:** Regularly review logs and error messages to ensure sensitive information isn't being inadvertently exposed.
-This type of credential exposure is extremely common in production applications and represents one of the most frequent ways database credentials are accidentally leaked.
\ No newline at end of file
+This type of credential exposure is extremely common in production applications and represents one of the most frequent ways database credentials are accidentally leaked.
diff --git a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge57ControllerTest.java b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge57ControllerTest.java
index f91d4bff1..9492f4760 100644
--- a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge57ControllerTest.java
+++ b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge57ControllerTest.java
@@ -28,4 +28,4 @@ void triggerDatabaseErrorShouldReturnErrorMessage() {
assertThat(result).isEqualTo(expectedError);
org.mockito.Mockito.verify(challenge).simulateDatabaseConnectionError();
}
-}
\ No newline at end of file
+}
diff --git a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge57Test.java b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge57Test.java
index 8c80ad0b2..0ea33f215 100644
--- a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge57Test.java
+++ b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge57Test.java
@@ -37,11 +37,11 @@ void answerCorrectShouldTrimWhitespace() {
void simulateDatabaseConnectionErrorShouldExposeConnectionString() {
var challenge = new Challenge57();
String errorMessage = challenge.simulateDatabaseConnectionError();
-
+
// Verify that the error message contains the exposed connection string
assertThat(errorMessage).contains("jdbc:postgresql://db.example.com:5432/userdb");
assertThat(errorMessage).contains("user=dbadmin");
assertThat(errorMessage).contains("password=SuperSecretDB2024!");
assertThat(errorMessage).contains("Database connection failed");
}
-}
\ No newline at end of file
+}
From cc5f015d655a457906566d3a3053f7b6fbe3d293 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 2 Aug 2025 23:24:13 +0000
Subject: [PATCH 04/17] Fix GitHub Pages workflow artifact naming conflict
- Give unique names to artifacts in different jobs (github-pages-preview vs github-pages-cleanup)
- Add explicit artifact_name parameter to deploy-pages actions
- Resolves "Multiple artifacts named 'github-pages'" error in workflow runs
Co-authored-by: commjoen <1457214+commjoen@users.noreply.github.com>
---
.github/workflows/github-pages-preview.yml | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/.github/workflows/github-pages-preview.yml b/.github/workflows/github-pages-preview.yml
index 2ad8e2bb9..66a66a555 100644
--- a/.github/workflows/github-pages-preview.yml
+++ b/.github/workflows/github-pages-preview.yml
@@ -211,10 +211,13 @@ jobs:
uses: actions/upload-pages-artifact@v3
with:
path: ./static-site
+ name: github-pages-preview
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+ with:
+ artifact_name: github-pages-preview
- name: Comment PR with preview link
uses: actions/github-script@v7
@@ -318,10 +321,13 @@ jobs:
uses: actions/upload-pages-artifact@v3
with:
path: ./static-site
+ name: github-pages-cleanup
- name: Deploy cleaned pages
id: deployment
uses: actions/deploy-pages@v4
+ with:
+ artifact_name: github-pages-cleanup
- name: Comment PR cleanup completion
uses: actions/github-script@v7
From 161cec1c873e432e25bea631b3858b17f35bb657 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 2 Aug 2025 23:55:45 +0000
Subject: [PATCH 05/17] Implement Challenge 58 and update Challenge 57 with
complete GitHub Pages preview support
Co-authored-by: commjoen <1457214+commjoen@users.noreply.github.com>
---
.../scripts/generate_thymeleaf_previews.py | 342 ++++-
.github/workflows/github-pages-preview.yml | 8 +
README.md | 2 +-
.../challenges/docker/Challenge57.java | 131 +-
.../docker/Challenge57Controller.java | 62 +-
.../challenges/docker/Challenge58.java | 60 +
.../docker/Challenge58Controller.java | 26 +
.../resources/explanations/challenge57.adoc | 48 +-
.../explanations/challenge57_hint.adoc | 36 +-
.../explanations/challenge57_reason.adoc | 66 +-
.../resources/explanations/challenge58.adoc | 33 +
.../explanations/challenge58_hint.adoc | 25 +
.../explanations/challenge58_reason.adoc | 40 +
.../wrong-secrets-configuration.yaml | 13 +
.../docker/Challenge57ControllerTest.java | 24 +-
.../challenges/docker/Challenge57Test.java | 23 +-
.../docker/Challenge58ControllerTest.java | 31 +
.../challenges/docker/Challenge58Test.java | 47 +
static-site/pr-123/pages/about.html | 436 +++++++
static-site/pr-123/pages/challenge-57.html | 124 ++
static-site/pr-123/pages/challenge-58.html | 131 ++
.../pr-123/pages/challenge-example.html | 128 ++
static-site/pr-123/pages/stats.html | 73 ++
static-site/pr-123/pages/welcome.html | 1144 +++++++++++++++++
24 files changed, 2918 insertions(+), 135 deletions(-)
create mode 100644 src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge58.java
create mode 100644 src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge58Controller.java
create mode 100644 src/main/resources/explanations/challenge58.adoc
create mode 100644 src/main/resources/explanations/challenge58_hint.adoc
create mode 100644 src/main/resources/explanations/challenge58_reason.adoc
create mode 100644 src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge58ControllerTest.java
create mode 100644 src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge58Test.java
create mode 100644 static-site/pr-123/pages/about.html
create mode 100644 static-site/pr-123/pages/challenge-57.html
create mode 100644 static-site/pr-123/pages/challenge-58.html
create mode 100644 static-site/pr-123/pages/challenge-example.html
create mode 100644 static-site/pr-123/pages/stats.html
create mode 100644 static-site/pr-123/pages/welcome.html
diff --git a/.github/scripts/generate_thymeleaf_previews.py b/.github/scripts/generate_thymeleaf_previews.py
index d761f1ba7..4e3a301a2 100755
--- a/.github/scripts/generate_thymeleaf_previews.py
+++ b/.github/scripts/generate_thymeleaf_previews.py
@@ -55,32 +55,72 @@ def generate_mock_challenges(self):
"Find the secret in the container",
"Retrieve cloud instance metadata",
"Use AWS Parameter Store",
+ "Find the secret in SSM",
+ "Find the Docker secret",
+ "GitHub Actions secret",
+ "Find the K8s secret",
+ "Find the hardcoded secret",
+ "Front-end secret exposure",
+ "Bash history secret",
+ "Find the secret in logs",
+ "Find the encrypted secret",
+ "Binary analysis secret",
+ "Go binary secret",
+ "Rust binary secret",
+ "Front-end secret part 2",
+ "Web3 secret exposure",
+ "Smart contract secret",
+ "Binary secret 2",
+ "Terraform secret",
+ "GitHub issue secret",
+ "Log analysis secret",
+ "Web page secret",
+ "AI prompt injection",
+ "Container debugging",
+ "Password shucking",
+ "Random key generation",
+ "Vulnerability reporting",
+ "Binary without strings",
+ "Security test access",
+ "Git notes secret",
+ "Insecure encryption key",
+ "Encryption key storage",
+ "Password shucking 2",
+ "Audit event secret",
+ "Vault template injection",
+ "Multi-environment secret",
+ "Vault subkey challenge",
+ "HashiCorp Vault injection",
+ "Binary secret 3",
+ "Binary secret 4",
+ "AES MD5 cracking",
+ "Docker secret 2",
+ "Binary secret 5",
+ "Docker secret 3",
+ "Container debugging 2",
+ "Docker secret 4",
+ "SSH bastion secret",
+ "AI secret exposure",
+ "LLM API key exposure in JavaScript", # Challenge 57
+ "Database connection string exposure", # Challenge 58
]
difficulties = ["⭐", "⭐⭐", "⭐⭐⭐", "⭐⭐⭐⭐", "⭐⭐⭐⭐⭐"]
techs = [
- "DEVOPS",
- "GIT",
- "FRONTEND",
- "DEVOPS",
- "AWS",
- "AZURE",
- "DOCKER",
- "DOCKER",
- "AWS",
- "AWS",
+ "DEVOPS", "GIT", "FRONTEND", "DEVOPS", "AWS", "AZURE", "DOCKER", "DOCKER", "AWS", "AWS",
+ "AWS", "DOCKER", "CI/CD", "K8S", "DEVOPS", "FRONTEND", "DEVOPS", "LOGGING", "CRYPTO", "BINARY",
+ "BINARY", "BINARY", "FRONTEND", "WEB3", "WEB3", "BINARY", "TERRAFORM", "GIT", "LOGGING", "FRONTEND",
+ "AI", "DOCKER", "PASSWORD", "CRYPTO", "DOCS", "BINARY", "CI/CD", "GIT", "CRYPTO", "CRYPTO",
+ "PASSWORD", "LOGGING", "VAULT", "VAULT", "VAULT", "VAULT", "BINARY", "BINARY", "CRYPTO", "DOCKER",
+ "BINARY", "DOCKER", "DOCKER", "DOCKER", "DOCKER", "AI", "AI", "LOGGING"
]
environments = [
- "Docker",
- "Docker",
- "Docker",
- "Docker",
- "AWS",
- "Azure",
- "Docker",
- "Docker",
- "AWS",
- "AWS",
+ "Docker", "Docker", "Docker", "Docker", "AWS", "Azure", "Docker", "Docker", "AWS", "AWS",
+ "AWS", "Docker", "Docker", "K8s", "Docker", "Docker", "Docker", "Docker", "Docker", "Docker",
+ "Docker", "Docker", "Docker", "Docker", "Docker", "Docker", "AWS", "Docker", "Docker", "Docker",
+ "Docker", "K8s", "Docker", "Docker", "Docker", "Docker", "Docker", "Docker", "Docker", "Docker",
+ "Docker", "Docker", "K8s with Vault", "K8s with Vault", "K8s with Vault", "K8s with Vault", "Docker", "Docker", "Docker", "Docker",
+ "Docker", "Docker", "K8s", "Docker", "Docker", "Docker", "Docker", "Docker"
]
for i, name in enumerate(challenge_names):
@@ -416,6 +456,266 @@ def generate_stats_page(self):
return content
+ def generate_challenge_57_preview(self):
+ """Generate a specific preview page for Challenge 57 (LLM)."""
+ return """
+
+
This is a static preview of Challenge 57: LLM API Key Exposure in Client-Side JavaScript
+
+
+
+
+
🤖 Challenge 57: LLM API Key Exposure
+
+ AI
+ ⭐⭐
+
+
+
+
+
+
📋 Challenge Description
+
This challenge demonstrates a critical security vulnerability in modern AI-powered web applications: LLM API keys exposed in client-side JavaScript code.
+
+
As developers rush to integrate AI capabilities, many make the critical mistake of putting sensitive API credentials directly in browser-accessible code.
+
+
+ ⚠️ Vulnerability: API keys exposed in client-side JavaScript can lead to massive financial losses, service disruption, and data harvesting.
+
+
+
🎯 Your Mission
+
Find the exposed LLM API key in the client-side JavaScript code. Look for:
+
+
API keys starting with "sk-"
+
JavaScript variables storing credentials
+
Console.log statements with sensitive data
+
Authorization headers in network requests
+
+
+
+
+
💻 Code Preview
+
+// AI Chat Application - Client-side JavaScript
+class LLMChatApp {
+ constructor() {
+ // WARNING: This is a security anti-pattern!
+ // API keys should NEVER be exposed in client-side code
+ this.apiKey = 'sk-llm-api-key-abc123def456ghi789jkl012mno345pqr678stu901vwx234yzA';
+ this.apiEndpoint = 'https://api.example-llm.com/v1/chat/completions';
+ this.initializeChat();
+ }
+
+ async sendMessage() {
+ try {
+ const response = await fetch(this.apiEndpoint, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${this.apiKey}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ model: 'gpt-3.5-turbo',
+ messages: [{role: 'user', content: message}]
+ })
+ });
+ } catch (error) {
+ console.log('Failed request used API key:', this.apiKey);
+ }
+ }
+}
+
+// Debug: Log the API key for development (another anti-pattern!)
+console.log('Debug: LLM API Key = sk-llm-api-key-abc123def456ghi789jkl012mno345pqr678stu901vwx234yzA');
+
+
+
+
🔍 How to Explore:
+
+
Visit /llm-demo to see the vulnerable chat application
+
Open Developer Tools (F12)
+
Check the /llm-chat.js file
+
Monitor console for debug messages
+
Examine network requests
+
+
+
+
+
+
+
🏆 Learning Objectives
+
+
+ 💰 Financial Impact
+
LLM API calls can be extremely expensive. Exposed keys have led to bills of tens of thousands of dollars within hours.
+
+
+ 🔒 Security Risks
+
Attackers can use your API keys for data harvesting, service disruption, and generating harmful content.
+
+
+ 🛡️ Prevention
+
Always use server-side proxies, environment variables, and proper access controls for AI service credentials.
This challenge demonstrates one of the most common and dangerous ways secrets leak in real-world applications: database connection strings with embedded credentials exposed through error messages.
+
+
When applications fail to connect to databases, they often expose the full connection string (including usernames and passwords) in error messages, logs, or even user-facing interfaces.
+
+
+ ⚠️ Critical Risk: Database connection string exposure can lead to direct database access and complete data breaches.
+
+
+
🎯 Your Mission
+
Trigger a database connection error and find the exposed password. Look for:
+
+
JDBC connection URLs with embedded credentials
+
Error messages containing connection strings
+
Log entries with sensitive information
+
Patterns like password=SECRET
+
+
+
+
+
💻 Vulnerable Code Example
+
+// Simulated database connection string with embedded credentials
+private static final String DB_CONNECTION_STRING =
+ "jdbc:postgresql://db.example.com:5432/userdb?" +
+ "user=dbadmin&password=SuperSecretDB2024!&ssl=true";
+
+public String simulateDatabaseConnectionError() {
+ try {
+ // This will fail and expose the connection string
+ Connection conn = DriverManager.getConnection(DB_CONNECTION_STRING);
+ return "Connection successful";
+ } catch (SQLException e) {
+ // Poor error handling - exposing full connection string
+ String errorMessage =
+ "Database connection failed with connection string: " +
+ DB_CONNECTION_STRING + "\nError: " + e.getMessage();
+
+ // Credentials also get logged (another exposure vector)
+ log.error("Failed to connect to database: {}", errorMessage);
+
+ return errorMessage;
+ }
+}
+
+
+
+ Example Error Output:
+ Database connection failed with connection string: jdbc:postgresql://db.example.com:5432/userdb?user=dbadmin&password=SuperSecretDB2024!&ssl=true
+
+
+
+
+
+
🔍 How to Trigger the Error
+
Visit the /error-demo/database-connection endpoint to simulate a database connection failure that exposes credentials in both the HTTP response and application logs.
+
+
+
+
🏆 Learning Objectives
+
+
+ 🚨 Common Exposure Vectors
+
+
Application startup logs
+
Health check failures
+
CI/CD pipeline logs
+
Error tracking services
+
+
+
+ 💥 Real-World Impact
+
+
Production database compromises
+
Complete data breaches
+
Lateral movement attacks
+
Compliance violations
+
+
+
+ 🛡️ Prevention
+
+
External secret management
+
Error message sanitization
+
Separate database credentials
+
Connection pooling
+
+
+
+
+
+
+
+"""
+
def generate_challenge_page(self):
"""Generate an example challenge page."""
template_path = self.templates_dir / "challenge.html"
@@ -552,6 +852,8 @@ def generate_all_pages(self):
"about.html": self.generate_about_page(),
"stats.html": self.generate_stats_page(),
"challenge-example.html": self.generate_challenge_page(),
+ "challenge-57.html": self.generate_challenge_57_preview(),
+ "challenge-58.html": self.generate_challenge_58_preview(),
}
for filename, content in pages.items():
diff --git a/.github/workflows/github-pages-preview.yml b/.github/workflows/github-pages-preview.yml
index 66a66a555..a31a8caf7 100644
--- a/.github/workflows/github-pages-preview.yml
+++ b/.github/workflows/github-pages-preview.yml
@@ -112,6 +112,9 @@ jobs:
ℹ️ About Page
+
+ 🤖 Challenge 57: LLM API Key Exposure
+
@@ -240,6 +246,8 @@ jobs:
- ℹ️ [About Page](${previewUrl}pages/about.html)
- 📊 [Stats & Config Page](${previewUrl}pages/stats.html)
- 🧩 [Challenge Example](${previewUrl}pages/challenge-example.html)
+ - 🤖 [Challenge 57: LLM API Key Exposure](${previewUrl}pages/challenge-57.html)
+ - 🗄️ [Challenge 58: Database Connection Exposure](${previewUrl}pages/challenge-58.html)
**For full functionality testing:** Use the [Docker preview](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) instead.
diff --git a/README.md b/README.md
index ae01ddba9..102de0a1e 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@
Welcome to the OWASP WrongSecrets game! The game is packed with real life examples of how to _not_ store secrets in your software. Each of these examples is captured in a challenge, which you need to solve using various tools and techniques. Solving these challenges will help you recognize common mistakes & can help you to reflect on your own secrets management strategy.
-Can you solve all the 57 challenges?
+Can you solve all the 58 challenges?
Try some of them on [our Heroku demo environment](https://wrongsecrets.herokuapp.com/).
diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge57.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge57.java
index a585f688b..24e2341e7 100644
--- a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge57.java
+++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge57.java
@@ -1,60 +1,121 @@
package org.owasp.wrongsecrets.challenges.docker;
import com.google.common.base.Strings;
-import java.sql.Connection;
-import java.sql.DriverManager;
-import java.sql.SQLException;
import lombok.extern.slf4j.Slf4j;
import org.owasp.wrongsecrets.challenges.Challenge;
import org.owasp.wrongsecrets.challenges.Spoiler;
import org.springframework.stereotype.Component;
-/** Challenge demonstrating database connection string exposure through error messages. */
+/** Challenge demonstrating LLM API key exposure in client-side JavaScript. */
@Slf4j
@Component
public class Challenge57 implements Challenge {
- // Simulated database connection string with embedded credentials
- private static final String DB_CONNECTION_STRING =
- "jdbc:postgresql://db.example.com:5432/userdb?user=dbadmin&password=SuperSecretDB2024!&ssl=true";
-
- private static final String EXPECTED_SECRET = "SuperSecretDB2024!";
+ // Simulated LLM API key that would be exposed in client-side JavaScript
+ private static final String LLM_API_KEY = "sk-llm-api-key-abc123def456ghi789jkl012mno345pqr678stu901vwx234yzA";
@Override
public Spoiler spoiler() {
- return new Spoiler(EXPECTED_SECRET);
+ return new Spoiler(LLM_API_KEY);
}
@Override
public boolean answerCorrect(String answer) {
- return !Strings.isNullOrEmpty(answer) && EXPECTED_SECRET.equals(answer.trim());
+ return !Strings.isNullOrEmpty(answer) && LLM_API_KEY.equals(answer.trim());
}
/**
- * This method simulates what happens when an application tries to connect to a database but
- * fails, exposing the full connection string (including credentials) in error messages. This is a
- * common real-world mistake where developers include sensitive information in connection strings
- * and don't properly handle/sanitize database connection errors.
+ * This method returns JavaScript code that would typically be served to the browser,
+ * containing an exposed LLM API key. This demonstrates how sensitive API keys can be
+ * accidentally exposed in client-side code.
*/
- public String simulateDatabaseConnectionError() {
- try {
- // This will fail since we don't have a real database, but it demonstrates
- // how connection errors can expose credentials
- Connection conn = DriverManager.getConnection(DB_CONNECTION_STRING);
- conn.close();
- return "Connection successful";
- } catch (SQLException e) {
- // Poor error handling - exposing the full connection string in the error message
- String errorMessage =
- "Database connection failed with connection string: "
- + DB_CONNECTION_STRING
- + "\nError: "
- + e.getMessage();
-
- // Log the error (another way credentials get exposed)
- log.error("Failed to connect to database: {}", errorMessage);
-
- return errorMessage;
- }
+ public String getLLMJavaScriptCode() {
+ return """
+ // AI Chat Application - Client-side JavaScript
+ class LLMChatApp {
+ constructor() {
+ // WARNING: This is a security anti-pattern!
+ // API keys should NEVER be exposed in client-side code
+ this.apiKey = '%s';
+ this.apiEndpoint = 'https://api.example-llm.com/v1/chat/completions';
+ this.initializeChat();
+ }
+
+ async initializeChat() {
+ console.log('Initializing LLM chat with API key:', this.apiKey);
+ this.setupEventListeners();
+ }
+
+ setupEventListeners() {
+ document.getElementById('send-button').addEventListener('click', () => {
+ this.sendMessage();
+ });
+
+ document.getElementById('message-input').addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') {
+ this.sendMessage();
+ }
+ });
+ }
+
+ async sendMessage() {
+ const messageInput = document.getElementById('message-input');
+ const message = messageInput.value.trim();
+
+ if (!message) return;
+
+ try {
+ const response = await fetch(this.apiEndpoint, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${this.apiKey}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ model: 'gpt-3.5-turbo',
+ messages: [
+ {role: 'user', content: message}
+ ],
+ max_tokens: 150
+ })
+ });
+
+ const data = await response.json();
+ this.displayResponse(data.choices[0].message.content);
+
+ } catch (error) {
+ console.error('LLM API Error:', error);
+ console.log('Failed request used API key:', this.apiKey);
+ this.displayError('Failed to get response from LLM service');
+ }
+
+ messageInput.value = '';
+ }
+
+ displayResponse(text) {
+ const chatOutput = document.getElementById('chat-output');
+ const responseDiv = document.createElement('div');
+ responseDiv.className = 'llm-response';
+ responseDiv.textContent = text;
+ chatOutput.appendChild(responseDiv);
+ chatOutput.scrollTop = chatOutput.scrollHeight;
+ }
+
+ displayError(error) {
+ const chatOutput = document.getElementById('chat-output');
+ const errorDiv = document.createElement('div');
+ errorDiv.className = 'error-message';
+ errorDiv.textContent = error;
+ chatOutput.appendChild(errorDiv);
+ }
+ }
+
+ // Initialize the chat app when page loads
+ document.addEventListener('DOMContentLoaded', () => {
+ // Debug: Log the API key for development (another anti-pattern!)
+ console.log('Debug: LLM API Key = %s');
+ new LLMChatApp();
+ });
+ """.formatted(LLM_API_KEY, LLM_API_KEY);
}
}
diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge57Controller.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge57Controller.java
index 7926bac4c..31a8525f2 100644
--- a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge57Controller.java
+++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge57Controller.java
@@ -2,10 +2,11 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
-/** REST controller for Challenge 57 to trigger database connection error. */
+/** REST controller for Challenge 57 to serve LLM JavaScript with exposed API key. */
@Slf4j
@RestController
@RequiredArgsConstructor
@@ -14,13 +15,58 @@ public class Challenge57Controller {
private final Challenge57 challenge;
/**
- * Endpoint to trigger a database connection error that exposes connection string with
- * credentials. This simulates what happens when applications try to connect to unavailable
- * databases.
+ * Endpoint to serve JavaScript code that contains an exposed LLM API key.
+ * This simulates how developers accidentally expose API keys in client-side code.
*/
- @GetMapping("/error-demo/database-connection")
- public String triggerDatabaseError() {
- log.info("Attempting database connection for Challenge 57...");
- return challenge.simulateDatabaseConnectionError();
+ @GetMapping(value = "/llm-chat.js", produces = MediaType.APPLICATION_JAVASCRIPT_VALUE)
+ public String getLLMJavaScript() {
+ log.info("Serving LLM JavaScript for Challenge 57...");
+ return challenge.getLLMJavaScriptCode();
+ }
+
+ /**
+ * Endpoint to serve a simple HTML page that loads the vulnerable JavaScript.
+ */
+ @GetMapping(value = "/llm-demo", produces = MediaType.TEXT_HTML_VALUE)
+ public String getLLMDemoPage() {
+ return """
+
+
+
+
+
+ LLM Chat Demo - Challenge 57
+
+
+
+
🤖 LLM Chat Demo
+
+ ⚠️ Security Notice: This demo application contains a common security vulnerability.
+ Can you find the exposed API key?
+
+
+
+
+
+
+
+
+
+
Hint: Check the browser's developer tools (F12) and look at the Network tab or Console.
+
You can also view the source code of the JavaScript file directly.
+
+
+
+
+
+ """;
}
}
diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge58.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge58.java
new file mode 100644
index 000000000..7fd538f16
--- /dev/null
+++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge58.java
@@ -0,0 +1,60 @@
+package org.owasp.wrongsecrets.challenges.docker;
+
+import com.google.common.base.Strings;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import lombok.extern.slf4j.Slf4j;
+import org.owasp.wrongsecrets.challenges.Challenge;
+import org.owasp.wrongsecrets.challenges.Spoiler;
+import org.springframework.stereotype.Component;
+
+/** Challenge demonstrating database connection string exposure through error messages. */
+@Slf4j
+@Component
+public class Challenge58 implements Challenge {
+
+ // Simulated database connection string with embedded credentials
+ private static final String DB_CONNECTION_STRING =
+ "jdbc:postgresql://db.example.com:5432/userdb?user=dbadmin&password=SuperSecretDB2024!&ssl=true";
+
+ private static final String EXPECTED_SECRET = "SuperSecretDB2024!";
+
+ @Override
+ public Spoiler spoiler() {
+ return new Spoiler(EXPECTED_SECRET);
+ }
+
+ @Override
+ public boolean answerCorrect(String answer) {
+ return !Strings.isNullOrEmpty(answer) && EXPECTED_SECRET.equals(answer.trim());
+ }
+
+ /**
+ * This method simulates what happens when an application tries to connect to a database but
+ * fails, exposing the full connection string (including credentials) in error messages. This is a
+ * common real-world mistake where developers include sensitive information in connection strings
+ * and don't properly handle/sanitize database connection errors.
+ */
+ public String simulateDatabaseConnectionError() {
+ try {
+ // This will fail since we don't have a real database, but it demonstrates
+ // how connection errors can expose credentials
+ Connection conn = DriverManager.getConnection(DB_CONNECTION_STRING);
+ conn.close();
+ return "Connection successful";
+ } catch (SQLException e) {
+ // Poor error handling - exposing the full connection string in the error message
+ String errorMessage =
+ "Database connection failed with connection string: "
+ + DB_CONNECTION_STRING
+ + "\nError: "
+ + e.getMessage();
+
+ // Log the error (another way credentials get exposed)
+ log.error("Failed to connect to database: {}", errorMessage);
+
+ return errorMessage;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge58Controller.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge58Controller.java
new file mode 100644
index 000000000..609db6eb8
--- /dev/null
+++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge58Controller.java
@@ -0,0 +1,26 @@
+package org.owasp.wrongsecrets.challenges.docker;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/** REST controller for Challenge 58 to trigger database connection error. */
+@Slf4j
+@RestController
+@RequiredArgsConstructor
+public class Challenge58Controller {
+
+ private final Challenge58 challenge;
+
+ /**
+ * Endpoint to trigger a database connection error that exposes connection string with
+ * credentials. This simulates what happens when applications try to connect to unavailable
+ * databases.
+ */
+ @GetMapping("/error-demo/database-connection")
+ public String triggerDatabaseError() {
+ log.info("Attempting database connection for Challenge 58...");
+ return challenge.simulateDatabaseConnectionError();
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/explanations/challenge57.adoc b/src/main/resources/explanations/challenge57.adoc
index 378fd5a45..668ab1acb 100644
--- a/src/main/resources/explanations/challenge57.adoc
+++ b/src/main/resources/explanations/challenge57.adoc
@@ -1,33 +1,37 @@
-=== Database Connection String Exposure in Error Messages
+=== LLM API Key Exposure in Client-Side JavaScript
-One of the most common and dangerous ways secrets leak in real-world applications is through database connection strings that contain embedded credentials. When applications fail to connect to databases, they often expose the full connection string (including usernames and passwords) in error messages, logs, or even user-facing interfaces.
+As AI and Large Language Model (LLM) integrations become increasingly popular in web applications, a new class of security vulnerabilities has emerged: **LLM API key exposure in client-side code**. This challenge demonstrates one of the most dangerous mistakes developers make when building AI-powered applications.
-This challenge demonstrates a scenario where a developer:
+This challenge simulates a scenario where a developer:
-1. **Uses embedded credentials in connection strings** instead of external secret management
-2. **Has poor error handling** that exposes the full connection string when database connections fail
-3. **Logs sensitive information** without sanitizing credentials first
-4. **Displays technical details** that could reach monitoring systems, error tracking tools, or even end users
+1. **Embeds LLM API keys directly in client-side JavaScript** instead of using server-side proxy endpoints
+2. **Exposes sensitive API credentials** to anyone who can view the source code
+3. **Logs API keys in browser console** for debugging purposes
+4. **Makes API keys accessible** to browser extensions, XSS attacks, and web scraping
-**Common places where these exposed connection strings appear:**
+**Why This Is Dangerous:**
-- Application startup logs when database is unavailable
-- Exception stack traces in monitoring tools like Sentry, Rollbar, or CloudWatch
-- Error messages displayed to users during maintenance windows
-- CI/CD pipeline logs when deployment health checks fail
-- Docker container logs during orchestration failures
+- **Financial Impact:** Exposed LLM API keys can be used by attackers to make expensive API calls, leading to huge bills
+- **Rate Limit Abuse:** Attackers can exhaust your API quotas, causing service disruptions
+- **Data Extraction:** Malicious actors can use your API keys to extract training data or perform reconnaissance
+- **Brand Damage:** Unauthorized use of your API keys can be associated with malicious activities
-**Real-world examples:**
+**Common Real-World Scenarios:**
-- Applications that fail health checks during Kubernetes deployments
-- Microservices that can't reach their database during startup
-- Database migration scripts that fail with exposed connection details
-- Development/testing environments where error verbosity is set too high
+- Chat applications that call OpenAI, Anthropic, or Google AI APIs directly from the browser
+- JavaScript code that includes API keys for development convenience
+- Single-page applications (SPAs) with hardcoded credentials
+- Debug console logs that accidentally expose API keys
+- Source maps that reveal API keys from minified code
-**How to trigger the error:**
+**How to Explore:**
-Visit the `/error-demo/database-connection` endpoint to simulate a database connection failure. This endpoint attempts to connect to a database using a connection string with embedded credentials, and when it fails, it exposes the credentials in both the HTTP response and application logs.
+1. Visit `/llm-demo` to see the vulnerable chat application
+2. Open your browser's Developer Tools (F12)
+3. Look for exposed API keys in the JavaScript source code
+4. Check the Console tab for any logged credentials
+5. Examine the Network tab to see how API keys are transmitted
-Can you find the database password that gets exposed when the application tries to connect to the database?
+Can you find the exposed LLM API key that would allow unauthorized access to the AI service?
-**Hint:** Look for database connection error messages that reveal more than they should.
+**Hint:** The secret you're looking for starts with "sk-" and is a typical OpenAI-style API key format.
diff --git a/src/main/resources/explanations/challenge57_hint.adoc b/src/main/resources/explanations/challenge57_hint.adoc
index fef53235a..db4b05db4 100644
--- a/src/main/resources/explanations/challenge57_hint.adoc
+++ b/src/main/resources/explanations/challenge57_hint.adoc
@@ -1 +1,35 @@
-Try visiting the `/error-demo/database-connection` endpoint to trigger a database connection error. Look at both the HTTP response and the application logs - database connection failures often expose connection strings with embedded credentials in error messages.
+=== Hint for Challenge 57
+
+This challenge focuses on a critical security flaw in modern AI-powered web applications: **LLM API keys exposed in client-side JavaScript code**.
+
+**Where to Look:**
+
+1. **Visit the demo page:** Go to `/llm-demo` to see the vulnerable chat application
+2. **Open Developer Tools:** Press F12 to open your browser's developer tools
+3. **Check JavaScript source:** Look at the `/llm-chat.js` file that's loaded by the page
+4. **Monitor the console:** Watch for any debug messages that might expose credentials
+5. **Examine network requests:** See how API keys are transmitted in HTTP headers
+
+**What to Look For:**
+
+- API keys that start with "sk-" (typical OpenAI format)
+- JavaScript variables that store authentication credentials
+- Console.log statements that might expose sensitive data
+- Authorization headers in network requests
+
+**Common Exposure Patterns:**
+
+- `this.apiKey = "sk-..."` in JavaScript classes
+- `console.log()` statements with API keys for debugging
+- `Authorization: Bearer ${apiKey}` in fetch requests
+- Hardcoded credentials in client-side configuration objects
+
+**Real-World Context:**
+
+This vulnerability is becoming increasingly common as developers integrate AI services like:
+- OpenAI GPT models
+- Anthropic Claude
+- Google Gemini/Bard
+- Azure OpenAI Service
+
+**Remember:** The goal is to find the LLM API key that's exposed in the client-side JavaScript code.
diff --git a/src/main/resources/explanations/challenge57_reason.adoc b/src/main/resources/explanations/challenge57_reason.adoc
index 6d6aa51df..6dceb673d 100644
--- a/src/main/resources/explanations/challenge57_reason.adoc
+++ b/src/main/resources/explanations/challenge57_reason.adoc
@@ -1,43 +1,47 @@
-=== What went wrong?
+=== Why Challenge 57 Matters: LLM API Key Exposure in Client-Side Code
-The application demonstrates several critical security mistakes commonly found in real-world applications:
+**The Problem:**
-**1. Embedded Credentials in Connection Strings**
-The database connection string contains the username and password directly embedded:
-```
-jdbc:postgresql://db.example.com:5432/userdb?user=dbadmin&password=SuperSecretDB2024!&ssl=true
-```
+This challenge highlights a rapidly growing security concern in the age of AI: **LLM API keys exposed in client-side JavaScript code**. As developers rush to integrate AI capabilities into their applications, many are making a critical mistake by putting sensitive API credentials directly in browser-accessible code.
-**2. Poor Error Handling**
-When the database connection fails, the application exposes the entire connection string in the error message, revealing the embedded credentials to anyone who can see the error.
+**Why This Happens:**
-**3. Sensitive Information in Logs**
-The error gets logged with the full connection string, meaning the credentials are now stored in log files that may be:
-- Accessible to multiple team members
-- Shipped to centralized logging systems
-- Stored long-term in log archives
-- Exposed through log monitoring tools
+1. **Rapid Development:** Developers want to quickly prototype AI features without setting up proper backend infrastructure
+2. **Convenience:** It's easier to call LLM APIs directly from the frontend than to create server-side proxy endpoints
+3. **Lack of Awareness:** Many developers don't realize that client-side code is completely public and inspectable
+4. **Debug Practices:** API keys get logged to console during development and forgotten in production code
-**4. No Secret Sanitization**
-The application doesn't sanitize sensitive information before logging or displaying errors.
+**Real-World Impact:**
-**How to fix this:**
+- **Massive Financial Losses:** LLM API calls can be extremely expensive. Exposed keys have led to bills of tens of thousands of dollars within hours
+- **Service Disruption:** Attackers can exhaust your API rate limits, making your application unusable
+- **Data Harvesting:** Malicious actors can use your API keys to extract information from LLM services
+- **Reputation Damage:** Your API keys could be used for generating harmful content associated with your account
-1. **Use external secret management:** Store database credentials in environment variables, secret management systems (AWS Secrets Manager, Azure Key Vault, HashiCorp Vault), or configuration files not checked into version control.
+**Recent Examples:**
-2. **Separate connection parameters:** Keep the connection URL separate from credentials:
- ```
- DB_HOST=db.example.com
- DB_PORT=5432
- DB_NAME=userdb
- DB_USER=dbadmin
- DB_PASS=SuperSecretDB2024!
- ```
+- Startups losing $10,000+ overnight due to exposed OpenAI API keys
+- GitHub repositories with hardcoded API keys being scraped by bots
+- Chrome extensions harvesting API keys from web applications
+- Competitor analysis tools extracting LLM prompts and responses using exposed credentials
-3. **Implement proper error handling:** Sanitize error messages before logging or displaying them. Never include connection strings or other sensitive data in error messages.
+**Attack Vectors:**
-4. **Use connection pooling with secure configuration:** Modern frameworks like Spring Boot support secure configuration of connection pools without exposing credentials.
+1. **Source Code Inspection:** Anyone can view client-side JavaScript source
+2. **Browser Extension Attacks:** Malicious extensions can steal API keys from memory
+3. **XSS Exploitation:** Cross-site scripting attacks can extract API keys
+4. **Automated Scanning:** Bots continuously scan websites for exposed credentials
+5. **Web Scraping:** Automated tools can extract API keys from thousands of sites
-5. **Monitor and audit:** Regularly review logs and error messages to ensure sensitive information isn't being inadvertently exposed.
+**Prevention Best Practices:**
-This type of credential exposure is extremely common in production applications and represents one of the most frequent ways database credentials are accidentally leaked.
+1. **Server-Side Proxy:** Always proxy LLM API calls through your backend
+2. **Environment Variables:** Use server-side environment variables for API keys
+3. **Rate Limiting:** Implement your own rate limiting and usage controls
+4. **Token-Based Auth:** Use short-lived tokens instead of permanent API keys
+5. **Content Security Policy:** Implement CSP headers to limit script execution
+6. **Regular Scanning:** Use tools to scan your codebase for exposed credentials
+
+**The Bottom Line:**
+
+LLM API keys should NEVER appear in client-side code. The financial and security risks are simply too high. Always use server-side proxies to protect your AI service credentials and implement proper access controls.
diff --git a/src/main/resources/explanations/challenge58.adoc b/src/main/resources/explanations/challenge58.adoc
new file mode 100644
index 000000000..2957c96c3
--- /dev/null
+++ b/src/main/resources/explanations/challenge58.adoc
@@ -0,0 +1,33 @@
+=== Database Connection String Exposure in Error Messages
+
+One of the most common and dangerous ways secrets leak in real-world applications is through database connection strings that contain embedded credentials. When applications fail to connect to databases, they often expose the full connection string (including usernames and passwords) in error messages, logs, or even user-facing interfaces.
+
+This challenge demonstrates a scenario where a developer:
+
+1. **Uses embedded credentials in connection strings** instead of external secret management
+2. **Has poor error handling** that exposes the full connection string when database connections fail
+3. **Logs sensitive information** without sanitizing credentials first
+4. **Displays technical details** that could reach monitoring systems, error tracking tools, or even end users
+
+**Common places where these exposed connection strings appear:**
+
+- Application startup logs when database is unavailable
+- Exception stack traces in monitoring tools like Sentry, Rollbar, or CloudWatch
+- Error messages displayed to users during maintenance windows
+- CI/CD pipeline logs when deployment health checks fail
+- Docker container logs during orchestration failures
+
+**Real-world examples:**
+
+- Applications that fail health checks during Kubernetes deployments
+- Microservices that can't reach their database during startup
+- Database migration scripts that fail with exposed connection details
+- Development/testing environments where error verbosity is set too high
+
+**How to trigger the error:**
+
+Visit the `/error-demo/database-connection` endpoint to simulate a database connection failure. This endpoint attempts to connect to a database using a connection string with embedded credentials, and when it fails, it exposes the credentials in both the HTTP response and application logs.
+
+Can you find the database password that gets exposed when the application tries to connect to the database?
+
+**Hint:** Look for database connection error messages that reveal more than they should.
\ No newline at end of file
diff --git a/src/main/resources/explanations/challenge58_hint.adoc b/src/main/resources/explanations/challenge58_hint.adoc
new file mode 100644
index 000000000..ca9a67eba
--- /dev/null
+++ b/src/main/resources/explanations/challenge58_hint.adoc
@@ -0,0 +1,25 @@
+=== Hint for Challenge 58
+
+This challenge demonstrates a very common security anti-pattern: **database connection strings with embedded credentials that leak through error messages**.
+
+**Where to look:**
+
+1. **Try the error endpoint:** Visit `/error-demo/database-connection` to trigger a database connection failure
+2. **Check the response:** The application will attempt to connect to a database and fail, exposing the connection string
+3. **Look at logs:** The application also logs the error with the exposed credentials
+
+**What to look for:**
+
+- JDBC connection URLs often contain sensitive information
+- Look for patterns like `jdbc:postgresql://...?user=USERNAME&password=PASSWORD`
+- The error message will show the full connection string when the database connection fails
+
+**Real-world context:**
+
+This is one of the most common ways secrets leak in production:
+- Database connection failures during application startup
+- Health check failures in container orchestration
+- Development environments with verbose error reporting
+- CI/CD pipelines where database connections fail
+
+**Remember:** The goal is to find the database password that gets exposed in the error message when the connection fails.
\ No newline at end of file
diff --git a/src/main/resources/explanations/challenge58_reason.adoc b/src/main/resources/explanations/challenge58_reason.adoc
new file mode 100644
index 000000000..819590381
--- /dev/null
+++ b/src/main/resources/explanations/challenge58_reason.adoc
@@ -0,0 +1,40 @@
+=== Why Challenge 58 Matters: Database Connection String Exposure
+
+**The Problem:**
+
+This challenge exposes one of the most frequent and dangerous secret leakage scenarios in real-world applications: **database connection strings with embedded credentials that get exposed through poor error handling**.
+
+**Why This Happens:**
+
+1. **Embedded Credentials:** Developers put usernames and passwords directly in connection strings instead of using environment variables or secret management systems
+2. **Poor Error Handling:** Applications don't sanitize error messages before logging or displaying them
+3. **Verbose Error Reporting:** Development practices leak into production with detailed error messages
+4. **Wide Distribution:** Error messages reach logs, monitoring systems, error tracking tools, and sometimes even end users
+
+**Real-World Impact:**
+
+- **Production Database Compromises:** Exposed credentials lead to direct database access
+- **Data Breaches:** Once database credentials are leaked, all stored data is at risk
+- **Lateral Movement:** Database access can be used to pivot to other systems
+- **Compliance Violations:** Exposed credentials violate security standards and regulations
+
+**Common Exposure Vectors:**
+
+- Application startup logs when database is unavailable
+- Health check failures in Kubernetes/Docker environments
+- CI/CD pipeline logs during deployment failures
+- Error tracking services (Sentry, Rollbar, Bugsnag)
+- CloudWatch, Azure Monitor, or Google Cloud Logging
+- Support tickets when users report errors
+
+**Prevention:**
+
+1. **External Secret Management:** Use environment variables, AWS Secrets Manager, Azure Key Vault, or HashiCorp Vault
+2. **Connection String Sanitization:** Strip credentials from error messages before logging
+3. **Least Privilege Logging:** Don't log connection strings or technical details that could contain secrets
+4. **Separate Database Users:** Use read-only users for applications that don't need write access
+5. **Connection Pooling:** Use connection pooling libraries that handle credentials securely
+
+**The Bottom Line:**
+
+Database connection string exposure is preventable with proper secret management practices and careful error handling. This vulnerability type has led to countless production breaches and should never occur in well-designed applications.
\ No newline at end of file
diff --git a/src/main/resources/wrong-secrets-configuration.yaml b/src/main/resources/wrong-secrets-configuration.yaml
index 0bc8a3097..afc69325d 100644
--- a/src/main/resources/wrong-secrets-configuration.yaml
+++ b/src/main/resources/wrong-secrets-configuration.yaml
@@ -889,6 +889,19 @@ configurations:
reason: "explanations/challenge57_reason.adoc"
environments: *all_envs
difficulty: *normal
+ category: *ai
+ ctf:
+ enabled: true
+
+ - name: Challenge 58
+ short-name: "challenge-58"
+ sources:
+ - class-name: "org.owasp.wrongsecrets.challenges.docker.Challenge58"
+ explanation: "explanations/challenge58.adoc"
+ hint: "explanations/challenge58_hint.adoc"
+ reason: "explanations/challenge58_reason.adoc"
+ environments: *all_envs
+ difficulty: *normal
category: *logging
ctf:
enabled: true
diff --git a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge57ControllerTest.java b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge57ControllerTest.java
index 9492f4760..304aea545 100644
--- a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge57ControllerTest.java
+++ b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge57ControllerTest.java
@@ -16,16 +16,28 @@ class Challenge57ControllerTest {
@InjectMocks private Challenge57Controller controller;
@Test
- void triggerDatabaseErrorShouldReturnErrorMessage() {
+ void getLLMJavaScriptShouldReturnJavaScriptCode() {
// Given
- String expectedError = "Database connection failed with connection string: ...";
- org.mockito.Mockito.when(challenge.simulateDatabaseConnectionError()).thenReturn(expectedError);
+ String expectedJs = "// AI Chat Application - Client-side JavaScript\nclass LLMChatApp {";
+ org.mockito.Mockito.when(challenge.getLLMJavaScriptCode()).thenReturn(expectedJs);
// When
- String result = controller.triggerDatabaseError();
+ String result = controller.getLLMJavaScript();
// Then
- assertThat(result).isEqualTo(expectedError);
- org.mockito.Mockito.verify(challenge).simulateDatabaseConnectionError();
+ assertThat(result).isEqualTo(expectedJs);
+ org.mockito.Mockito.verify(challenge).getLLMJavaScriptCode();
+ }
+
+ @Test
+ void getLLMDemoPageShouldReturnHTMLPage() {
+ // When
+ String result = controller.getLLMDemoPage();
+
+ // Then
+ assertThat(result).contains("");
+ assertThat(result).contains("LLM Chat Demo");
+ assertThat(result).contains("Security Notice");
+ assertThat(result).contains("/llm-chat.js");
}
}
diff --git a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge57Test.java b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge57Test.java
index 0ea33f215..a09d775b1 100644
--- a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge57Test.java
+++ b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge57Test.java
@@ -10,13 +10,13 @@ class Challenge57Test {
@Test
void spoilerShouldReturnCorrectAnswer() {
var challenge = new Challenge57();
- assertThat(challenge.spoiler()).isEqualTo(new Spoiler("SuperSecretDB2024!"));
+ assertThat(challenge.spoiler()).isEqualTo(new Spoiler("sk-llm-api-key-abc123def456ghi789jkl012mno345pqr678stu901vwx234yzA"));
}
@Test
void answerCorrectShouldReturnTrueForCorrectAnswer() {
var challenge = new Challenge57();
- assertThat(challenge.answerCorrect("SuperSecretDB2024!")).isTrue();
+ assertThat(challenge.answerCorrect("sk-llm-api-key-abc123def456ghi789jkl012mno345pqr678stu901vwx234yzA")).isTrue();
}
@Test
@@ -30,18 +30,19 @@ void answerCorrectShouldReturnFalseForIncorrectAnswer() {
@Test
void answerCorrectShouldTrimWhitespace() {
var challenge = new Challenge57();
- assertThat(challenge.answerCorrect(" SuperSecretDB2024! ")).isTrue();
+ assertThat(challenge.answerCorrect(" sk-llm-api-key-abc123def456ghi789jkl012mno345pqr678stu901vwx234yzA ")).isTrue();
}
@Test
- void simulateDatabaseConnectionErrorShouldExposeConnectionString() {
+ void getLLMJavaScriptCodeShouldExposeAPIKey() {
var challenge = new Challenge57();
- String errorMessage = challenge.simulateDatabaseConnectionError();
-
- // Verify that the error message contains the exposed connection string
- assertThat(errorMessage).contains("jdbc:postgresql://db.example.com:5432/userdb");
- assertThat(errorMessage).contains("user=dbadmin");
- assertThat(errorMessage).contains("password=SuperSecretDB2024!");
- assertThat(errorMessage).contains("Database connection failed");
+ String jsCode = challenge.getLLMJavaScriptCode();
+
+ // Verify that the JavaScript code contains the exposed API key
+ assertThat(jsCode).contains("sk-llm-api-key-abc123def456ghi789jkl012mno345pqr678stu901vwx234yzA");
+ assertThat(jsCode).contains("this.apiKey = ");
+ assertThat(jsCode).contains("console.log");
+ assertThat(jsCode).contains("Authorization");
+ assertThat(jsCode).contains("Bearer");
}
}
diff --git a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge58ControllerTest.java b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge58ControllerTest.java
new file mode 100644
index 000000000..f4674f596
--- /dev/null
+++ b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge58ControllerTest.java
@@ -0,0 +1,31 @@
+package org.owasp.wrongsecrets.challenges.docker;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class Challenge58ControllerTest {
+
+ @Mock private Challenge58 challenge;
+
+ @InjectMocks private Challenge58Controller controller;
+
+ @Test
+ void triggerDatabaseErrorShouldReturnErrorMessage() {
+ // Given
+ String expectedError = "Database connection failed with connection string: ...";
+ org.mockito.Mockito.when(challenge.simulateDatabaseConnectionError()).thenReturn(expectedError);
+
+ // When
+ String result = controller.triggerDatabaseError();
+
+ // Then
+ assertThat(result).isEqualTo(expectedError);
+ org.mockito.Mockito.verify(challenge).simulateDatabaseConnectionError();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge58Test.java b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge58Test.java
new file mode 100644
index 000000000..cc044ab49
--- /dev/null
+++ b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge58Test.java
@@ -0,0 +1,47 @@
+package org.owasp.wrongsecrets.challenges.docker;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+import org.owasp.wrongsecrets.challenges.Spoiler;
+
+class Challenge58Test {
+
+ @Test
+ void spoilerShouldReturnCorrectAnswer() {
+ var challenge = new Challenge58();
+ assertThat(challenge.spoiler()).isEqualTo(new Spoiler("SuperSecretDB2024!"));
+ }
+
+ @Test
+ void answerCorrectShouldReturnTrueForCorrectAnswer() {
+ var challenge = new Challenge58();
+ assertThat(challenge.answerCorrect("SuperSecretDB2024!")).isTrue();
+ }
+
+ @Test
+ void answerCorrectShouldReturnFalseForIncorrectAnswer() {
+ var challenge = new Challenge58();
+ assertThat(challenge.answerCorrect("wronganswer")).isFalse();
+ assertThat(challenge.answerCorrect("")).isFalse();
+ assertThat(challenge.answerCorrect(null)).isFalse();
+ }
+
+ @Test
+ void answerCorrectShouldTrimWhitespace() {
+ var challenge = new Challenge58();
+ assertThat(challenge.answerCorrect(" SuperSecretDB2024! ")).isTrue();
+ }
+
+ @Test
+ void simulateDatabaseConnectionErrorShouldExposeConnectionString() {
+ var challenge = new Challenge58();
+ String errorMessage = challenge.simulateDatabaseConnectionError();
+
+ // Verify that the error message contains the exposed connection string
+ assertThat(errorMessage).contains("jdbc:postgresql://db.example.com:5432/userdb");
+ assertThat(errorMessage).contains("user=dbadmin");
+ assertThat(errorMessage).contains("password=SuperSecretDB2024!");
+ assertThat(errorMessage).contains("Database connection failed");
+ }
+}
\ No newline at end of file
diff --git a/static-site/pr-123/pages/about.html b/static-site/pr-123/pages/about.html
new file mode 100644
index 000000000..e3ad99f48
--- /dev/null
+++ b/static-site/pr-123/pages/about.html
@@ -0,0 +1,436 @@
+
+
+
+
+
+
📋 Static Preview Notice
+ This is a static preview of PR #123. Some dynamic content may be simplified or use mock data.
+
+
About
+
+
+
+
About WrongSecrets
+
+ This app started as a bad example app for a talk for AllDayDevops in 2020, "DevSecOps — Our Secret
+ Management Journey from Code to Vault". How an organisation handles its secrets reflects its security maturity,
+ yet secrets management is not a "sexy" topic even within security. Many security breaches have their roots in improper management of secrets.
+ It turns out people do want some more guidance on it!
+
+
+ Hence, we reworked the code base of this project and filed for it to become an OWASP project.
+ Our goal is to educate people about secrets management and its pitfalls while they have a good time learning!
+
+
+
+
🎯 Learning Objectives
+
After completing the challenges, you'll understand:
+
+
Common places where secrets are accidentally exposed
+
How to properly store and manage secrets
+
Security best practices for different environments (local, cloud, containers)
+
How to detect and fix secret management vulnerabilities
+
+
Perfect for developers, security professionals, and DevOps engineers!
+
+
+
+ We hope you can better assess and implement proper secrets management after going through the challenges and explanations in our app.
+ Have fun, and remember to star us on GitHub!
+
+
+ If you like WrongSecrets and its mission, please consider supporting OWASP in our name!
+
+
Copyright (c) 2020-2025 Jeroen Willemsen and WrongSecrets contributors.
+
Licensed under AGPLv3
+
+
+
+
+
Licenses
+
+ The list below is generated with `mvn license:add-third-party`
+
+
+
Lists of 362 third-party dependencies.
+
(Eclipse Public License - v 1.0) (GNU Lesser General Public License) Logback Classic Module (ch.qos.logback:logback-classic:1.5.18 - http://logback.qos.ch/logback-classic)
+
(Eclipse Public License - v 1.0) (GNU Lesser General Public License) Logback Core Module (ch.qos.logback:logback-core:1.5.18 - http://logback.qos.ch/logback-core)
+
(The MIT License (MIT)) Microsoft Azure Java Core Library (com.azure:azure-core:1.55.3 - https://github.com/Azure/azure-sdk-for-java)
+
(The MIT License (MIT)) Microsoft Azure Java Core AMQP Library (com.azure:azure-core-amqp:2.9.16 - https://github.com/Azure/azure-sdk-for-java)
+
(The MIT License (MIT)) Microsoft Azure Netty HTTP Client Library (com.azure:azure-core-http-netty:1.15.11 - https://github.com/Azure/azure-sdk-for-java)
+
(The MIT License (MIT)) Microsoft Azure Management Java Core Library (com.azure:azure-core-management:1.17.0 - https://github.com/Azure/azure-sdk-for-java)
+
(The MIT License (MIT)) Microsoft Azure client library for Identity (com.azure:azure-identity:1.15.4 - https://github.com/Azure/azure-sdk-for-java)
+
(The MIT License (MIT)) Microsoft Azure Java JSON Library (com.azure:azure-json:1.5.0 - https://github.com/Azure/azure-sdk-for-java)
+
(The MIT License (MIT)) Microsoft Azure client library for KeyVault Secrets (com.azure:azure-security-keyvault-secrets:4.9.4 - https://github.com/Azure/azure-sdk-for-java)
+
(The MIT License (MIT)) Microsoft Azure Java XML Library (com.azure:azure-xml:1.2.0 - https://github.com/Azure/azure-sdk-for-java)
+
(The MIT License (MIT)) Spring Cloud Azure AutoConfigure (com.azure.spring:spring-cloud-azure-autoconfigure:5.22.0 - https://microsoft.github.io/spring-cloud-azure)
+
(The MIT License (MIT)) Spring Cloud Azure Core (com.azure.spring:spring-cloud-azure-core:5.22.0 - https://microsoft.github.io/spring-cloud-azure)
+
(The MIT License (MIT)) Spring Cloud Azure Service (com.azure.spring:spring-cloud-azure-service:5.22.0 - https://microsoft.github.io/spring-cloud-azure)
+
(The MIT License (MIT)) Spring Cloud Azure Starter (com.azure.spring:spring-cloud-azure-starter:5.22.0 - https://microsoft.github.io/spring-cloud-azure)
+
(The MIT License (MIT)) Spring Cloud Azure Starter Key Vault Secrets (com.azure.spring:spring-cloud-azure-starter-keyvault-secrets:5.22.0 - https://microsoft.github.io/spring-cloud-azure)
+
(The Apache Software License, Version 2.0) Simple XML (safe) (com.carrotsearch.thirdparty:simple-xml-safe:2.7.1 - https://github.com/dweiss/simplexml)
(Apache License, Version 2.0) Internet Time Utility (com.ethlo.time:itu:1.10.3 - https://github.com/ethlo/itu)
+
(The Apache Software License, Version 2.0) aalto-xml (com.fasterxml:aalto-xml:1.3.3 - https://github.com/FasterXML/aalto-xml)
+
(Apache License, Version 2.0) ClassMate (com.fasterxml:classmate:1.7.0 - https://github.com/FasterXML/java-classmate)
+
(The Apache Software License, Version 2.0) Jackson-annotations (com.fasterxml.jackson.core:jackson-annotations:2.19.1 - https://github.com/FasterXML/jackson)
+
(The Apache Software License, Version 2.0) Jackson-core (com.fasterxml.jackson.core:jackson-core:2.19.1 - https://github.com/FasterXML/jackson-core)
+
(The Apache Software License, Version 2.0) jackson-databind (com.fasterxml.jackson.core:jackson-databind:2.19.1 - https://github.com/FasterXML/jackson)
+
(The Apache Software License, Version 2.0) Jackson-dataformat-XML (com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.19.1 - https://github.com/FasterXML/jackson-dataformat-xml)
+
(The Apache Software License, Version 2.0) Jackson-dataformat-YAML (com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.19.1 - https://github.com/FasterXML/jackson-dataformats-text)
+
(The Apache Software License, Version 2.0) Jackson datatype: jdk8 (com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.19.1 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8)
+
(The Apache Software License, Version 2.0) Jackson datatype: JSR310 (com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.19.1 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310)
+
(The Apache Software License, Version 2.0) Jackson module: Blackbird (com.fasterxml.jackson.module:jackson-module-blackbird:2.19.1 - https://github.com/FasterXML/jackson-modules-base)
+
(The Apache Software License, Version 2.0) Jackson-module-parameter-names (com.fasterxml.jackson.module:jackson-module-parameter-names:2.19.1 - https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names)
+
(The Apache License, Version 2.0) Woodstox (com.fasterxml.woodstox:woodstox-core:7.0.0 - https://github.com/FasterXML/woodstox)
+
(GNU Lesser General Public License version 3) (The Apache Software License, Version 2.0) jffi (com.github.jnr:jffi:1.3.13 - http://github.com/jnr/jffi)
+
(The Apache Software License, Version 2.0) jnr-a64asm (com.github.jnr:jnr-a64asm:1.0.0 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-a64asm)
+
(The Apache Software License, Version 2.0) jnr-constants (com.github.jnr:jnr-constants:0.10.4 - http://github.com/jnr/jnr-constants)
+
(The Apache Software License, Version 2.0) jnr-enxio (com.github.jnr:jnr-enxio:0.32.18 - http://github.com/jnr/jnr-enxio)
+
(The Apache Software License, Version 2.0) jnr-ffi (com.github.jnr:jnr-ffi:2.2.17 - http://github.com/jnr/jnr-ffi)
+
(The Apache Software License, Version 2.0) jnr-netdb (com.github.jnr:jnr-netdb:1.2.0 - http://github.com/jnr/jnr-netdb)
+
(Eclipse Public License - v 2.0) (GNU General Public License Version 2) (GNU Lesser General Public License Version 2.1) jnr-posix (com.github.jnr:jnr-posix:3.1.20 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-posix)
+
(The Apache Software License, Version 2.0) jnr-unixsocket (com.github.jnr:jnr-unixsocket:0.38.23 - http://github.com/jnr/jnr-unixsocket)
(BSD New license) Google Auth Library for Java - Credentials (com.google.auth:google-auth-library-credentials:1.37.1 - https://github.com/googleapis/google-auth-library-java/google-auth-library-credentials)
+
(BSD New license) Google Auth Library for Java - OAuth2 HTTP (com.google.auth:google-auth-library-oauth2-http:1.37.1 - https://github.com/googleapis/google-auth-library-java/google-auth-library-oauth2-http)
(The Apache Software License, Version 2.0) Guava InternalFutureFailureAccess and InternalFutures (com.google.guava:failureaccess:1.0.2 - https://github.com/google/guava/failureaccess)
+
(Apache License, Version 2.0) Guava: Google Core Libraries for Java (com.google.guava:guava:33.4.0-jre - https://github.com/google/guava)
+
(The Apache Software License, Version 2.0) Guava ListenableFuture only (com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava - https://github.com/google/guava/listenablefuture)
+
(The Apache Software License, Version 2.0) Google HTTP Client Library for Java (com.google.http-client:google-http-client:1.47.1 - https://github.com/googleapis/google-http-java-client/google-http-client)
+
(The Apache Software License, Version 2.0) GSON extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-gson:1.47.1 - https://github.com/googleapis/google-http-java-client/google-http-client-gson)
+
(Apache License, Version 2.0) J2ObjC Annotations (com.google.j2objc:j2objc-annotations:3.0.0 - https://github.com/google/j2objc/)
(The Apache Software License, Version 2.0) retirejs-core (com.h3xstream.retirejs:retirejs-core:3.0.4 - https://github.com/h3xstream/burp-retire-js/retirejs-core)
+
(Apache License Version 2.0) AhoCorasickDoubleArrayTrie (com.hankcs:aho-corasick-double-array-trie:1.2.3 - https://github.com/hankcs/AhoCorasickDoubleArrayTrie)
+
(The Apache Software License, Version 2.0) backport9 (com.headius:backport9:1.13 - http://nexus.sonatype.org/oss-repository-hosting.html/backport9)
+
(The Apache Software License, Version 2.0) invokebinder (com.headius:invokebinder:1.14 - http://maven.apache.org)
+
(The Apache Software License, Version 2.0) options (com.headius:options:1.6 - https://github.com/headius/options)
(The MIT License (MIT)) Extensions on Apache Proton-J library (com.microsoft.azure:qpid-proton-j-extensions:1.2.5 - https://github.com/Azure/qpid-proton-j-extensions)
+
(The MIT License) toml4j (com.moandjiezana.toml:toml4j:0.7.2 - http://moandjiezana.com/toml/toml4j)
+
(Apache License Version 2.0) JsonSchemaValidator (com.networknt:json-schema-validator:1.5.6 - https://github.com/networknt/json-schema-validator)
+
(The Apache Software License, Version 2.0) Nimbus Content Type (com.nimbusds:content-type:2.3 - https://bitbucket.org/connect2id/nimbus-content-type)
+
(The Apache Software License, Version 2.0) Nimbus LangTag (com.nimbusds:lang-tag:1.7 - https://bitbucket.org/connect2id/nimbus-language-tags)
+
(The Apache Software License, Version 2.0) Nimbus JOSE+JWT (com.nimbusds:nimbus-jose-jwt:10.0.1 - https://bitbucket.org/connect2id/nimbus-jose-jwt)
+
(Apache License, version 2.0) OAuth 2.0 SDK with OpenID Connect extensions (com.nimbusds:oauth2-oidc-sdk:11.23 - https://bitbucket.org/connect2id/oauth-2.0-sdk-with-openid-connect-extensions)
+
(The (New) BSD License) jmustache (com.samskivert:jmustache:1.16 - http://github.com/samskivert/jmustache)
+
(Eclipse Distribution License - v 1.0) Old JAXB Core (com.sun.xml.bind:jaxb-core:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/)
+
(Eclipse Distribution License - v 1.0) Old JAXB Runtime (com.sun.xml.bind:jaxb-impl:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/)
(The Apache License, Version 2.0) jcs3-slf4j (io.github.jeremylong:jcs3-slf4j:1.0.5 - https://github.com/jeremylong/jcs3-slf4j/)
+
(The Apache License, Version 2.0) open-vulnerability-clients (io.github.jeremylong:open-vulnerability-clients:7.3.2 - https://github.com/jeremylong/vuln-tools/)
(The Apache Software License, Version 2.0) micrometer-commons (io.micrometer:micrometer-commons:1.15.1 - https://github.com/micrometer-metrics/micrometer)
+
(The Apache Software License, Version 2.0) micrometer-core (io.micrometer:micrometer-core:1.15.1 - https://github.com/micrometer-metrics/micrometer)
+
(The Apache Software License, Version 2.0) micrometer-jakarta9 (io.micrometer:micrometer-jakarta9:1.15.1 - https://github.com/micrometer-metrics/micrometer)
+
(The Apache Software License, Version 2.0) micrometer-observation (io.micrometer:micrometer-observation:1.15.1 - https://github.com/micrometer-metrics/micrometer)
+
(Apache License, Version 2.0) Netty/Buffer (io.netty:netty-buffer:4.1.118.Final - https://netty.io/netty-buffer/)
+
(Apache License, Version 2.0) Netty/Codec (io.netty:netty-codec:4.1.118.Final - https://netty.io/netty-codec/)
+
(Apache License, Version 2.0) Netty/Codec/DNS (io.netty:netty-codec-dns:4.1.118.Final - https://netty.io/netty-codec-dns/)
+
(Apache License, Version 2.0) Netty/Codec/HTTP (io.netty:netty-codec-http:4.1.118.Final - https://netty.io/netty-codec-http/)
+
(Apache License, Version 2.0) Netty/Codec/HTTP2 (io.netty:netty-codec-http2:4.1.118.Final - https://netty.io/netty-codec-http2/)
+
(Apache License, Version 2.0) Netty/Codec/Socks (io.netty:netty-codec-socks:4.1.118.Final - https://netty.io/netty-codec-socks/)
+
(Apache License, Version 2.0) Netty/Common (io.netty:netty-common:4.1.118.Final - https://netty.io/netty-common/)
+
(Apache License, Version 2.0) Netty/Handler (io.netty:netty-handler:4.1.118.Final - https://netty.io/netty-handler/)
+
(Apache License, Version 2.0) Netty/Handler/Proxy (io.netty:netty-handler-proxy:4.1.118.Final - https://netty.io/netty-handler-proxy/)
+
(Apache License, Version 2.0) Netty/Resolver (io.netty:netty-resolver:4.1.118.Final - https://netty.io/netty-resolver/)
+
(Apache License, Version 2.0) Netty/Resolver/DNS (io.netty:netty-resolver-dns:4.1.118.Final - https://netty.io/netty-resolver-dns/)
+
(Apache License, Version 2.0) Netty/Resolver/DNS/Classes/MacOS (io.netty:netty-resolver-dns-classes-macos:4.1.118.Final - https://netty.io/netty-resolver-dns-classes-macos/)
+
(Apache License, Version 2.0) Netty/Resolver/DNS/Native/MacOS (io.netty:netty-resolver-dns-native-macos:4.1.118.Final - https://netty.io/netty-resolver-dns-native-macos/)
+
(The Apache Software License, Version 2.0) Netty/TomcatNative [BoringSSL - Static] (io.netty:netty-tcnative-boringssl-static:2.0.70.Final - https://github.com/netty/netty-tcnative/netty-tcnative-boringssl-static/)
+
(The Apache Software License, Version 2.0) Netty/TomcatNative [OpenSSL - Classes] (io.netty:netty-tcnative-classes:2.0.70.Final - https://github.com/netty/netty-tcnative/netty-tcnative-classes/)
+
(Apache License, Version 2.0) Netty/Transport (io.netty:netty-transport:4.1.118.Final - https://netty.io/netty-transport/)
+
(Apache License, Version 2.0) Netty/Transport/Classes/Epoll (io.netty:netty-transport-classes-epoll:4.1.118.Final - https://netty.io/netty-transport-classes-epoll/)
+
(Apache License, Version 2.0) Netty/Transport/Classes/KQueue (io.netty:netty-transport-classes-kqueue:4.1.118.Final - https://netty.io/netty-transport-classes-kqueue/)
+
(Apache License, Version 2.0) Netty/Transport/Native/Epoll (io.netty:netty-transport-native-epoll:4.1.118.Final - https://netty.io/netty-transport-native-epoll/)
+
(Apache License, Version 2.0) Netty/Transport/Native/KQueue (io.netty:netty-transport-native-kqueue:4.1.118.Final - https://netty.io/netty-transport-native-kqueue/)
+
(Apache License, Version 2.0) Netty/Transport/Native/Unix/Common (io.netty:netty-transport-native-unix-common:4.1.118.Final - https://netty.io/netty-transport-native-unix-common/)
+
(The Apache License, Version 2.0) OpenCensus (io.opencensus:opencensus-api:0.31.1 - https://github.com/census-instrumentation/opencensus-java)
+
(The Apache License, Version 2.0) OpenCensus (io.opencensus:opencensus-contrib-http-util:0.31.1 - https://github.com/census-instrumentation/opencensus-java)
(Apache License, Version 2.0) Non-Blocking Reactive Foundation for the JVM (io.projectreactor:reactor-core:3.7.7 - https://github.com/reactor/reactor-core)
+
(The Apache Software License, Version 2.0) Core functionality for the Reactor Netty library (io.projectreactor.netty:reactor-netty-core:1.2.7 - https://github.com/reactor/reactor-netty)
+
(The Apache Software License, Version 2.0) HTTP functionality for the Reactor Netty library (io.projectreactor.netty:reactor-netty-http:1.2.7 - https://github.com/reactor/reactor-netty)
(EDL 1.0) Jakarta Activation API (jakarta.activation:jakarta.activation-api:2.1.3 - https://github.com/jakartaee/jaf-api)
+
(EPL 2.0) (GPL2 w/ CPE) Jakarta Annotations API (jakarta.annotation:jakarta.annotation-api:2.1.1 - https://projects.eclipse.org/projects/ee4j.ca)
+
(EPL 2.0) (GPL2 w/ CPE) jakarta.transaction API (jakarta.transaction:jakarta.transaction-api:2.0.1 - https://projects.eclipse.org/projects/ee4j.jta)
+
(Apache License 2.0) Jakarta Bean Validation API (jakarta.validation:jakarta.validation-api:3.0.2 - https://beanvalidation.org)
+
(Eclipse Distribution License - v 1.0) Jakarta XML Binding API (jakarta.xml.bind:jakarta.xml.bind-api:4.0.2 - https://github.com/jakartaee/jaxb-api/jakarta.xml.bind-api)
+
(CDDL/GPLv2+CE) JavaBeans Activation Framework API jar (javax.activation:javax.activation-api:1.2.0 - http://java.net/all/javax.activation-api/)
+
(CDDL + GPLv2 with classpath exception) javax.annotation API (javax.annotation:javax.annotation-api:1.3.2 - http://jcp.org/en/jsr/detail?id=250)
+
(The Apache Software License, Version 2.0) javax.inject (javax.inject:javax.inject:1 - http://code.google.com/p/atinject/)
(The Apache Software License, Version 2.0) ASM based accessors helper used by json-smart (net.minidev:accessors-smart:2.5.2 - https://urielch.github.io/)
+
(The Apache Software License, Version 2.0) JSON Small and Fast Parser (net.minidev:json-smart:2.5.2 - https://urielch.github.io/)
+
(The Apache Software License, Version 2.0) groovy-extensions (nz.net.ultraq.groovy:groovy-extensions:2.3.3 - https://github.com/ultraq/groovy-extensions/)
+
(The Apache Software License, Version 2.0) thymeleaf-expression-processor (nz.net.ultraq.thymeleaf:thymeleaf-expression-processor:3.2.0 - https://github.com/ultraq/thymeleaf-expression-processor/)
+
(The Apache Software License, Version 2.0) thymeleaf-layout-dialect (nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.4.0 - https://github.com/ultraq/thymeleaf-layout-dialect/)
+
(The Apache Software License, Version 2.0) OGNL - Object Graph Navigation Library (ognl:ognl:3.3.4 - https://github.com/jkuhnert/ognl/)
(The Apache Software License, Version 2.0) Maven Aether Provider (org.apache.maven:maven-aether-provider:3.0 - http://maven.apache.org/maven-aether-provider/)
+
(The Apache Software License, Version 2.0) Maven Artifact (org.apache.maven:maven-artifact:3.0 - http://maven.apache.org/maven-artifact/)
+
(The Apache Software License, Version 2.0) Maven Core (org.apache.maven:maven-core:3.0 - http://maven.apache.org/maven-core/)
+
(The Apache Software License, Version 2.0) Maven Model (org.apache.maven:maven-model:3.0 - http://maven.apache.org/maven-model/)
+
(The Apache Software License, Version 2.0) Maven Model Builder (org.apache.maven:maven-model-builder:3.0 - http://maven.apache.org/maven-model-builder/)
+
(The Apache Software License, Version 2.0) Maven Plugin API (org.apache.maven:maven-plugin-api:3.0 - http://maven.apache.org/maven-plugin-api/)
+
(The Apache Software License, Version 2.0) Maven Repository Metadata Model (org.apache.maven:maven-repository-metadata:3.0 - http://maven.apache.org/maven-repository-metadata/)
+
(The Apache Software License, Version 2.0) Maven Settings (org.apache.maven:maven-settings:3.0 - http://maven.apache.org/maven-settings/)
+
(The Apache Software License, Version 2.0) Maven Settings Builder (org.apache.maven:maven-settings-builder:3.0 - http://maven.apache.org/maven-settings-builder/)
+
(Apache-2.0) Doxia :: Sink API (org.apache.maven.doxia:doxia-sink-api:2.0.0 - https://maven.apache.org/doxia/doxia/doxia-sink-api/)
+
(Apache-2.0) Apache Maven Reporting API (org.apache.maven.reporting:maven-reporting-api:4.0.0 - https://maven.apache.org/shared/maven-reporting-api/)
+
(Apache License, Version 2.0) Maven Artifact Resolver API (org.apache.maven.resolver:maven-resolver-api:1.4.1 - https://maven.apache.org/resolver/maven-resolver-api/)
(The Apache Software License, Version 2.0) Plexus Interpolation API (org.codehaus.plexus:plexus-interpolation:1.14 - http://plexus.codehaus.org/plexus-components/plexus-interpolation)
+
(Apache License, Version 2.0) Plexus Common Utilities (org.codehaus.plexus:plexus-utils:4.0.2 - https://codehaus-plexus.github.io/plexus-utils/)
+
(Apache License, Version 2.0) Plexus XML Utilities (org.codehaus.plexus:plexus-xml:3.0.1 - https://codehaus-plexus.github.io/plexus-xml/)
+
(The BSD 2-Clause License) Stax2 API (org.codehaus.woodstox:stax2-api:4.2.2 - http://github.com/FasterXML/stax2-api)
(Eclipse Public License - Version 2.0) Eclipse Packager :: Core (org.eclipse.packager:packager-core:0.21.0 - https://eclipse.org/packager/packager-core)
+
(Eclipse Public License - Version 2.0) Eclipse Packager :: RPM (org.eclipse.packager:packager-rpm:0.21.0 - https://eclipse.org/packager/packager-rpm)
+
(Eclipse Public License 2.0) (GNU General Public License, version 2 with the GNU Classpath Exception) JSON-P Default Provider (org.glassfish:jakarta.json:2.0.1 - https://github.com/eclipse-ee4j/jsonp)
(The MIT License) jsoup Java HTML Parser (org.jsoup:jsoup:1.20.1 - https://jsoup.org/)
+
(The Apache License, Version 2.0) JSpecify annotations (org.jspecify:jspecify:1.0.0 - http://jspecify.org/)
+
(Public Domain, per Creative Commons CC0) LatencyUtils (org.latencyutils:LatencyUtils:2.0.3 - http://latencyutils.github.io/LatencyUtils/)
+
(Apache License, Version 2.0) KeePassJava2 :: All (org.linguafranca.pwdb:KeePassJava2:2.2.4 - https://github.com/jorabin/KeePassJava2/KeePassJava2)
+
(Apache License, Version 2.0) KeePassJava2 :: DOM (org.linguafranca.pwdb:KeePassJava2-dom:2.2.4 - https://github.com/jorabin/KeePassJava2/KeePassJava2-dom)
+
(Apache License, Version 2.0) KeePassJava2 :: Jackson (org.linguafranca.pwdb:KeePassJava2-jackson:2.2.4 - https://github.com/jorabin/KeePassJava2/KeePassJava2-jackson)
+
(Apache License, Version 2.0) KeePassJava2 :: JAXB (org.linguafranca.pwdb:KeePassJava2-jaxb:2.2.4 - https://github.com/jorabin/KeePassJava2/KeePassJava2-jaxb)
+
(Apache License, Version 2.0) KeePassJava2 :: KDB (org.linguafranca.pwdb:KeePassJava2-kdb:2.2.4 - https://github.com/jorabin/KeePassJava2/KeePassJava2-kdb)
+
(Apache License, Version 2.0) KeePassJava2 :: KDBX (org.linguafranca.pwdb:KeePassJava2-kdbx:2.2.4 - https://github.com/jorabin/KeePassJava2/KeePassJava2-kdbx)
+
(Apache License, Version 2.0) KeePassJava2 :: Simple (org.linguafranca.pwdb:KeePassJava2-simple:2.2.4 - https://github.com/jorabin/KeePassJava2/KeePassJava2-simple)
+
(Apache License, Version 2.0) PWDB :: Database (org.linguafranca.pwdb:database:2.2.4 - https://github.com/jorabin/KeePassJava2/database)
+
(Apache License, Version 2.0) PWDB :: Util (org.linguafranca.pwdb:util:2.2.4 - https://github.com/jorabin/KeePassJava2/util)
(The Apache Software License, Version 2.0) Sisu - Guice (org.sonatype.sisu:sisu-guice:2.1.7 - http://forge.sonatype.com/sisu-guice/)
+
(The Apache Software License, Version 2.0) Sisu - Inject (JSR330 bean support) (org.sonatype.sisu:sisu-inject-bean:1.4.2 - http://sisu.sonatype.org/sisu-inject/guice-bean/sisu-inject-bean/)
+
(The Apache Software License, Version 2.0) Sisu - Inject (Plexus bean support) (org.sonatype.sisu:sisu-inject-plexus:1.4.2 - http://sisu.sonatype.org/sisu-inject/guice-bean/guice-plexus/sisu-inject-plexus/)
+
(The Apache License, Version 2.0) springdoc-openapi-starter-common (org.springdoc:springdoc-openapi-starter-common:2.8.9 - https://springdoc.org/springdoc-openapi-starter-common/)
+
(The Apache License, Version 2.0) springdoc-openapi-starter-webmvc-api (org.springdoc:springdoc-openapi-starter-webmvc-api:2.8.9 - https://springdoc.org/springdoc-openapi-starter-webmvc-api/)
+
(The Apache License, Version 2.0) springdoc-openapi-starter-webmvc-ui (org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9 - https://springdoc.org/springdoc-openapi-starter-webmvc-ui/)
+
(Apache License, Version 2.0) Spring AOP (org.springframework:spring-aop:6.2.8 - https://github.com/spring-projects/spring-framework)
+
(Apache License, Version 2.0) Spring Beans (org.springframework:spring-beans:6.2.8 - https://github.com/spring-projects/spring-framework)
+
(Apache License, Version 2.0) Spring Context (org.springframework:spring-context:6.2.8 - https://github.com/spring-projects/spring-framework)
+
(Apache License, Version 2.0) Spring Core (org.springframework:spring-core:6.2.8 - https://github.com/spring-projects/spring-framework)
+
(Apache License, Version 2.0) Spring Expression Language (SpEL) (org.springframework:spring-expression:6.2.8 - https://github.com/spring-projects/spring-framework)
+
(Apache License, Version 2.0) Spring Commons Logging Bridge (org.springframework:spring-jcl:6.2.8 - https://github.com/spring-projects/spring-framework)
+
(Apache License, Version 2.0) Spring Web (org.springframework:spring-web:6.2.8 - https://github.com/spring-projects/spring-framework)
+
(Apache License, Version 2.0) Spring Web MVC (org.springframework:spring-webmvc:6.2.8 - https://github.com/spring-projects/spring-framework)
+
(Apache License, Version 2.0) spring-boot (org.springframework.boot:spring-boot:3.5.3 - https://spring.io/projects/spring-boot)
+
(Apache License, Version 2.0) spring-boot-actuator (org.springframework.boot:spring-boot-actuator:3.5.3 - https://spring.io/projects/spring-boot)
+
(Apache License, Version 2.0) spring-boot-actuator-autoconfigure (org.springframework.boot:spring-boot-actuator-autoconfigure:3.5.3 - https://spring.io/projects/spring-boot)
+
(Apache License, Version 2.0) spring-boot-autoconfigure (org.springframework.boot:spring-boot-autoconfigure:3.5.3 - https://spring.io/projects/spring-boot)
+
(Apache License, Version 2.0) spring-boot-starter (org.springframework.boot:spring-boot-starter:3.5.3 - https://spring.io/projects/spring-boot)
+
(Apache License, Version 2.0) spring-boot-starter-actuator (org.springframework.boot:spring-boot-starter-actuator:3.5.3 - https://spring.io/projects/spring-boot)
+
(Apache License, Version 2.0) spring-boot-starter-json (org.springframework.boot:spring-boot-starter-json:3.5.3 - https://spring.io/projects/spring-boot)
+
(Apache License, Version 2.0) spring-boot-starter-logging (org.springframework.boot:spring-boot-starter-logging:3.5.3 - https://spring.io/projects/spring-boot)
+
(Apache License, Version 2.0) spring-boot-starter-thymeleaf (org.springframework.boot:spring-boot-starter-thymeleaf:3.5.3 - https://spring.io/projects/spring-boot)
+
(Apache License, Version 2.0) spring-boot-starter-tomcat (org.springframework.boot:spring-boot-starter-tomcat:3.5.3 - https://spring.io/projects/spring-boot)
+
(Apache License, Version 2.0) spring-boot-starter-validation (org.springframework.boot:spring-boot-starter-validation:3.5.3 - https://spring.io/projects/spring-boot)
+
(Apache License, Version 2.0) spring-boot-starter-web (org.springframework.boot:spring-boot-starter-web:3.5.3 - https://spring.io/projects/spring-boot)
+
(Apache License, Version 2.0) Spring Cloud Commons (org.springframework.cloud:spring-cloud-commons:4.3.0 - https://projects.spring.io/spring-cloud/spring-cloud-commons/)
+
(Apache License, Version 2.0) Spring Cloud Context (org.springframework.cloud:spring-cloud-context:4.3.0 - https://projects.spring.io/spring-cloud/spring-cloud-context/)
+
(Apache License, Version 2.0) spring-cloud-starter (org.springframework.cloud:spring-cloud-starter:4.3.0 - https://projects.spring.io/spring-cloud)
+
(Apache License, Version 2.0) Spring Cloud Starter Vault Config (org.springframework.cloud:spring-cloud-starter-vault-config:4.3.0 - https://cloud.spring.io/spring-cloud-vault/)
+
(Apache License, Version 2.0) Spring Cloud Vault Configuration Integration (org.springframework.cloud:spring-cloud-vault-config:4.3.0 - https://spring.io/spring-cloud/spring-cloud-vault-parent/spring-cloud-vault-config)
+
(Apache License, Version 2.0) spring-security-config (org.springframework.security:spring-security-config:6.5.1 - https://spring.io/projects/spring-security)
+
(Apache License, Version 2.0) spring-security-core (org.springframework.security:spring-security-core:6.5.1 - https://spring.io/projects/spring-security)
+
(Apache License, Version 2.0) spring-security-crypto (org.springframework.security:spring-security-crypto:6.5.1 - https://spring.io/projects/spring-security)
+
(Apache License, Version 2.0) spring-security-web (org.springframework.security:spring-security-web:6.5.1 - https://spring.io/projects/spring-security)
+
(Apache License, Version 2.0) Spring Vault Core (org.springframework.vault:spring-vault-core:3.2.0 - https://projects.spring.io/spring-vault/spring-vault-core/)
(The Apache Software License, Version 2.0) thymeleaf (org.thymeleaf:thymeleaf:3.1.3.RELEASE - http://www.thymeleaf.org/thymeleaf-lib/thymeleaf)
+
(The Apache Software License, Version 2.0) thymeleaf-spring6 (org.thymeleaf:thymeleaf-spring6:3.1.3.RELEASE - http://www.thymeleaf.org/thymeleaf-lib/thymeleaf-spring6)
+
(The Apache Software License, Version 2.0) thymeleaf-extras-springsecurity6 (org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.3.RELEASE - http://www.thymeleaf.org/thymeleaf-lib/thymeleaf-extras-springsecurity6)
+
(Public Domain) XZ for Java (org.tukaani:xz:1.9 - https://tukaani.org/xz/java.html)
+
(The Apache Software License, Version 2.0) unbescape (org.unbescape:unbescape:1.1.6.RELEASE - http://www.unbescape.org)
+
(Apache License, Version 2.0) Bootstrap (org.webjars:bootstrap:5.3.7 - http://webjars.org)
(Apache License, Version 2.0) AWS Java SDK :: Third Party :: Jackson-core (software.amazon.awssdk:third-party-jackson-core:2.31.77 - https://aws.amazon.com/sdkforjava)
This is a static preview of Challenge 57: LLM API Key Exposure in Client-Side JavaScript
+
+
+
+
+
🤖 Challenge 57: LLM API Key Exposure
+
+ AI
+ ⭐⭐
+
+
+
+
+
+
📋 Challenge Description
+
This challenge demonstrates a critical security vulnerability in modern AI-powered web applications: LLM API keys exposed in client-side JavaScript code.
+
+
As developers rush to integrate AI capabilities, many make the critical mistake of putting sensitive API credentials directly in browser-accessible code.
+
+
+ ⚠️ Vulnerability: API keys exposed in client-side JavaScript can lead to massive financial losses, service disruption, and data harvesting.
+
+
+
🎯 Your Mission
+
Find the exposed LLM API key in the client-side JavaScript code. Look for:
+
+
API keys starting with "sk-"
+
JavaScript variables storing credentials
+
Console.log statements with sensitive data
+
Authorization headers in network requests
+
+
+
+
+
💻 Code Preview
+
+// AI Chat Application - Client-side JavaScript
+class LLMChatApp {
+ constructor() {
+ // WARNING: This is a security anti-pattern!
+ // API keys should NEVER be exposed in client-side code
+ this.apiKey = 'sk-llm-api-key-abc123def456ghi789jkl012mno345pqr678stu901vwx234yzA';
+ this.apiEndpoint = 'https://api.example-llm.com/v1/chat/completions';
+ this.initializeChat();
+ }
+
+ async sendMessage() {
+ try {
+ const response = await fetch(this.apiEndpoint, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${this.apiKey}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ model: 'gpt-3.5-turbo',
+ messages: [{role: 'user', content: message}]
+ })
+ });
+ } catch (error) {
+ console.log('Failed request used API key:', this.apiKey);
+ }
+ }
+}
+
+// Debug: Log the API key for development (another anti-pattern!)
+console.log('Debug: LLM API Key = sk-llm-api-key-abc123def456ghi789jkl012mno345pqr678stu901vwx234yzA');
+
+
+
+
🔍 How to Explore:
+
+
Visit /llm-demo to see the vulnerable chat application
+
Open Developer Tools (F12)
+
Check the /llm-chat.js file
+
Monitor console for debug messages
+
Examine network requests
+
+
+
+
+
+
+
🏆 Learning Objectives
+
+
+ 💰 Financial Impact
+
LLM API calls can be extremely expensive. Exposed keys have led to bills of tens of thousands of dollars within hours.
+
+
+ 🔒 Security Risks
+
Attackers can use your API keys for data harvesting, service disruption, and generating harmful content.
+
+
+ 🛡️ Prevention
+
Always use server-side proxies, environment variables, and proper access controls for AI service credentials.
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/static-site/pr-123/pages/challenge-58.html b/static-site/pr-123/pages/challenge-58.html
new file mode 100644
index 000000000..8d808d6f3
--- /dev/null
+++ b/static-site/pr-123/pages/challenge-58.html
@@ -0,0 +1,131 @@
+
+
+
+
+
+ Challenge 58: Database Connection String Exposure - Preview
+
+
+
+
+
+
+
+
📋 Challenge 58 Preview
+
This is a static preview of Challenge 58: Database Connection String Exposure through Error Messages
This challenge demonstrates one of the most common and dangerous ways secrets leak in real-world applications: database connection strings with embedded credentials exposed through error messages.
+
+
When applications fail to connect to databases, they often expose the full connection string (including usernames and passwords) in error messages, logs, or even user-facing interfaces.
+
+
+ ⚠️ Critical Risk: Database connection string exposure can lead to direct database access and complete data breaches.
+
+
+
🎯 Your Mission
+
Trigger a database connection error and find the exposed password. Look for:
+
+
JDBC connection URLs with embedded credentials
+
Error messages containing connection strings
+
Log entries with sensitive information
+
Patterns like password=SECRET
+
+
+
+
+
💻 Vulnerable Code Example
+
+// Simulated database connection string with embedded credentials
+private static final String DB_CONNECTION_STRING =
+ "jdbc:postgresql://db.example.com:5432/userdb?" +
+ "user=dbadmin&password=SuperSecretDB2024!&ssl=true";
+
+public String simulateDatabaseConnectionError() {
+ try {
+ // This will fail and expose the connection string
+ Connection conn = DriverManager.getConnection(DB_CONNECTION_STRING);
+ return "Connection successful";
+ } catch (SQLException e) {
+ // Poor error handling - exposing full connection string
+ String errorMessage =
+ "Database connection failed with connection string: " +
+ DB_CONNECTION_STRING + "
+Error: " + e.getMessage();
+
+ // Credentials also get logged (another exposure vector)
+ log.error("Failed to connect to database: {}", errorMessage);
+
+ return errorMessage;
+ }
+}
+
+
+
+ Example Error Output:
+ Database connection failed with connection string: jdbc:postgresql://db.example.com:5432/userdb?user=dbadmin&password=SuperSecretDB2024!&ssl=true
+
+
+
+
+
+
🔍 How to Trigger the Error
+
Visit the /error-demo/database-connection endpoint to simulate a database connection failure that exposes credentials in both the HTTP response and application logs.
+
+
+
+
🏆 Learning Objectives
+
+
+ 🚨 Common Exposure Vectors
+
+
Application startup logs
+
Health check failures
+
CI/CD pipeline logs
+
Error tracking services
+
+
+
+ 💥 Real-World Impact
+
+
Production database compromises
+
Complete data breaches
+
Lateral movement attacks
+
Compliance violations
+
+
+
+ 🛡️ Prevention
+
+
External secret management
+
Error message sanitization
+
Separate database credentials
+
Connection pooling
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/static-site/pr-123/pages/challenge-example.html b/static-site/pr-123/pages/challenge-example.html
new file mode 100644
index 000000000..7272568d4
--- /dev/null
+++ b/static-site/pr-123/pages/challenge-example.html
@@ -0,0 +1,128 @@
+
+
+
+
+
+
📋 Static Preview Notice
+ This is a static preview of PR #123. Some dynamic content may be simplified or use mock data.
+
+
+
Challenge 1: Find the hard-coded password< /> ⭐<>
+
+
+
+
+
+
🔍 Your Task
+
Find the secret hidden in the WrongSecrets repository. This challenge focuses on DEVOPS<>secret management.
+
💡 Look for: Configuration files, source code, environment variables, Docker files, or cloud infrastructure related to this challenge.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
[(${missingEnvWarning})]
+
+
+
+
+
+
+
Congratulations!
+
+
+
+
You have finished all the doable challenges! Congratulations!
+
We hope you have enjoyed the ride! And have learned something about the pitfalls in secrets
+ management.
Getting Started: Check out the GitHub repository to examine the code and find the secrets!
+
+
+
+ Pro Tip: Each challenge below has a different difficulty level and may require different environments.
+ Start with the easier ones and work your way up! 🚀
+
+
+
+
+
+
+
+ Difficulty: ⭐ (Easy) ⭐⭐ (Medium) ⭐⭐⭐ (Hard) ⭐⭐⭐⭐ (Expert) ⭐⭐⭐⭐⭐ (Master) |
+ Environment: Where the challenge can be solved
+
+
+ Wondering what a secret is? A secret is often a confidential piece of information that is
+ required to unlock certain functionalities or information. It can exists in many shapes or
+ forms, for instance:
+
This challenge demonstrates a critical security vulnerability in modern AI-powered web applications: LLM API keys exposed in client-side JavaScript code.
-
+
As developers rush to integrate AI capabilities, many make the critical mistake of putting sensitive API credentials directly in browser-accessible code.
This challenge demonstrates one of the most common and dangerous ways secrets leak in real-world applications: database connection strings with embedded credentials exposed through error messages.
-
+
When applications fail to connect to databases, they often expose the full connection string (including usernames and passwords) in error messages, logs, or even user-facing interfaces.
@@ -759,13 +759,13 @@ def generate_challenge_58_preview(self):
return "Connection successful";
} catch (SQLException e) {
// Poor error handling - exposing full connection string
- String errorMessage =
+ String errorMessage =
"Database connection failed with connection string: " +
DB_CONNECTION_STRING + "\nError: " + e.getMessage();
-
+
// Credentials also get logged (another exposure vector)
log.error("Failed to connect to database: {}", errorMessage);
-
+
return errorMessage;
}
}
diff --git a/config/zap/rule-config.tsv b/config/zap/rule-config.tsv
index 080e43759..8974a0bb6 100644
--- a/config/zap/rule-config.tsv
+++ b/config/zap/rule-config.tsv
@@ -18,3 +18,4 @@
10003 IGNORE Vulnerable JS Library
90004 IGNORE Insufficient Site Isolation Against Spectre Vulnerability
2 IGNORE Private IP Disclosure
+90022 IGNORE Application Error Disclosure
diff --git a/src/main/resources/explanations/challenge58.adoc b/src/main/resources/explanations/challenge58.adoc
index 2957c96c3..378fd5a45 100644
--- a/src/main/resources/explanations/challenge58.adoc
+++ b/src/main/resources/explanations/challenge58.adoc
@@ -30,4 +30,4 @@ Visit the `/error-demo/database-connection` endpoint to simulate a database conn
Can you find the database password that gets exposed when the application tries to connect to the database?
-**Hint:** Look for database connection error messages that reveal more than they should.
\ No newline at end of file
+**Hint:** Look for database connection error messages that reveal more than they should.
diff --git a/src/main/resources/explanations/challenge58_hint.adoc b/src/main/resources/explanations/challenge58_hint.adoc
index ca9a67eba..390c37037 100644
--- a/src/main/resources/explanations/challenge58_hint.adoc
+++ b/src/main/resources/explanations/challenge58_hint.adoc
@@ -22,4 +22,4 @@ This is one of the most common ways secrets leak in production:
- Development environments with verbose error reporting
- CI/CD pipelines where database connections fail
-**Remember:** The goal is to find the database password that gets exposed in the error message when the connection fails.
\ No newline at end of file
+**Remember:** The goal is to find the database password that gets exposed in the error message when the connection fails.
diff --git a/src/main/resources/explanations/challenge58_reason.adoc b/src/main/resources/explanations/challenge58_reason.adoc
index 819590381..54ea7abca 100644
--- a/src/main/resources/explanations/challenge58_reason.adoc
+++ b/src/main/resources/explanations/challenge58_reason.adoc
@@ -37,4 +37,4 @@ This challenge exposes one of the most frequent and dangerous secret leakage sce
**The Bottom Line:**
-Database connection string exposure is preventable with proper secret management practices and careful error handling. This vulnerability type has led to countless production breaches and should never occur in well-designed applications.
\ No newline at end of file
+Database connection string exposure is preventable with proper secret management practices and careful error handling. This vulnerability type has led to countless production breaches and should never occur in well-designed applications.
diff --git a/static-site/pr-123/pages/challenge-57.html b/static-site/pr-123/pages/challenge-57.html
index 06dc2bc5e..d8c882d9f 100644
--- a/static-site/pr-123/pages/challenge-57.html
+++ b/static-site/pr-123/pages/challenge-57.html
@@ -35,7 +35,7 @@
🤖 Challenge 57: LLM API Key Exposure
📋 Challenge Description
This challenge demonstrates a critical security vulnerability in modern AI-powered web applications: LLM API keys exposed in client-side JavaScript code.
-
+
As developers rush to integrate AI capabilities, many make the critical mistake of putting sensitive API credentials directly in browser-accessible code.