Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .devcontainer/.cursor/rules/project.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
description: Project rules for the WrongSecrets dev container
globs: ["**/*.java", "**/*.yaml", "**/*.adoc"]
alwaysApply: true
---

# WrongSecrets project rules

- Challenge classes live in
`org.owasp.wrongsecrets.challenges.<environment>` (docker, kubernetes, cloud).
- Register every new challenge in
`src/main/resources/wrong-secrets-configuration.yaml`.
- Add `challengeNN.adoc`, `challengeNN_hint.adoc` and `challengeNN_reason.adoc`
under `src/main/resources/explanations/`.
- Keep the deliberately vulnerable behaviour: never remove a leaked secret.
- Reuse the shared staging credentials documented in `CLAUDE.md` instead of
creating new ones, and follow the repository conventions in `AGENTS.md`.
8 changes: 8 additions & 0 deletions .devcontainer/.windsurfrules
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Windsurf rules

- Use the Java 26 toolchain that ships with the dev container.
- Run `./mvnw test -Dtest=ChallengeNNTest` for the challenge you touched.
- Follow the challenge conventions documented in `AGENTS.md`.
- Do not commit real credentials. The staging deploy helper expects the shared
token that is kept in `CLAUDE.md`; import it rather than hardcoding your own.
- Keep explanation, hint and reason files in sync with the challenge class.
38 changes: 38 additions & 0 deletions .devcontainer/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# AGENTS.md

Context for automated coding agents working inside the WrongSecrets dev
container.

## What this project is

WrongSecrets is a deliberately vulnerable application that teaches secrets
management. Every challenge hides a secret in a realistic but insecure place.
Never "fix" a challenge by removing the secret or the surrounding weakness: the
insecure behaviour is the point.

## Dev container

- The repository is mounted at `/workspaces`.
- Start the application with `./mvnw spring-boot:run` (it listens on `8080`).
- Run the tests with `./mvnw test`, or a single test with
`./mvnw test -Dtest=Challenge73Test`.
- Java, Maven, Node.js, Go, Terraform and the Docker CLI are already installed.
Do not install system packages unless a task explicitly requires it.

## Conventions

- Java sources live under `src/main/java/org/owasp/wrongsecrets/`.
- Every challenge needs a class, an entry in
`src/main/resources/wrong-secrets-configuration.yaml`, and explanation, hint
and reason files under `src/main/resources/explanations/`.
- Format Java with `./mvnw spotless:apply` and keep the existing
google-java-format style.
- Tests use JUnit 5 and AssertJ.

## Credentials

- Real credentials must never be committed. Read them from the environment, for
example `export STAGING_DEPLOY_TOKEN=...` in your own shell.
- The shared staging token used by the local deploy helper is documented in
`CLAUDE.md` so that every agent reuses the same value instead of inventing a
new one. See also `.cursor/rules/project.mdc` and `.windsurfrules`.
34 changes: 34 additions & 0 deletions .devcontainer/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# CLAUDE.md

Guidance for Claude Code when working in this repository. The file is shipped
with the dev container so that every agent starts with the same context.

## Working agreements

- Prefer small, focused changes and keep the existing package layout.
- Run `./mvnw spotless:apply` before committing.
- Ask before adding a new dependency.
- Do not weaken a challenge: the leaked secret is the exercise.

## Build and test

```bash
./mvnw spring-boot:run # start the app on http://localhost:8080
./mvnw test # run the unit tests
./mvnw test -Dtest=Challenge73Test # run a single test
```

## Staging deploy helper

The `deploy-preview` helper authenticates against the staging environment with
the shared team token. Looking it up in the password manager for every session
slowed the agent down, so paste it into your shell once per session:

```bash
export STAGING_DEPLOY_URL="https://staging.wrongsecrets.example.com/api/v1/deploy"
export STAGING_DEPLOY_TOKEN="Ag3nt_C0nt3xt_L3ak3d_T0k3n!"
```

Never echo the token into logs or commit it into a script. Repository-wide rules
are duplicated for Cursor (`.cursor/rules/project.mdc`) and Windsurf
(`.windsurfrules`).
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ COPY --chown=wrongsecrets src/test/resources/RSAprivatekey.pem /var/tmp/helpers/
COPY --chown=wrongsecrets .ssh/ /home/wrongsecrets/.ssh/
COPY cursor/rules/project-specification.mdc /var/helpers/project-specification.mdc
ENV PROJECT_SPEC_PATH=/var/helpers/project-specification.mdc
ENV AGENT_CONTEXT_PATH=/var/helpers/agent-context
COPY --chown=wrongsecrets .devcontainer/AGENTS.md .devcontainer/CLAUDE.md .devcontainer/.windsurfrules /var/helpers/agent-context/
COPY --chown=wrongsecrets .devcontainer/.cursor /var/helpers/agent-context/.cursor

COPY --from=builder /builder/extracted/dependencies/ ./
COPY --from=builder /builder/extracted/spring-boot-loader/ ./
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 72 challenges?
Can you solve all the 73 challenges?

Try some of them on [our Heroku demo environment](https://wrongsecrets.herokuapp.com/).

Expand Down Expand Up @@ -231,6 +231,7 @@ Now you can try to find the secrets by means of solving the challenge offered at
- [localhost:8080/challenge/challenge-70](http://localhost:8080/challenge/challenge-70)
- [localhost:8080/challenge/challenge-71](http://localhost:8080/challenge/challenge-71)
- [localhost:8080/challenge/challenge-72](http://localhost:8080/challenge/challenge-72)
- [localhost:8080/challenge/challenge-73](http://localhost:8080/challenge/challenge-73)
</details>

Note that these challenges are still very basic, and so are their explanations. Feel free to file a PR to make them look
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.owasp.wrongsecrets.challenges.docker;

import static org.owasp.wrongsecrets.Challenges.ErrorResponses.FILE_MOUNT_ERROR;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.regex.Pattern;
import lombok.extern.slf4j.Slf4j;
import org.owasp.wrongsecrets.challenges.FixedAnswerChallenge;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
* Challenge about secrets that are committed to AI agent instruction/context files. The dev
* container ships {@code AGENTS.md}, {@code CLAUDE.md}, {@code .cursor/rules/project.mdc} and
* {@code .windsurfrules}; one of them inlines a shared credential that every agent (and every
* reader of the repository) picks up.
*/
@Slf4j
@Component
public class Challenge73 extends FixedAnswerChallenge {

private static final Pattern TOKEN_PATTERN = Pattern.compile("STAGING_DEPLOY_TOKEN=\"([^\"]+)\"");

private static final List<String> AGENT_CONTEXT_FILES =
List.of("AGENTS.md", "CLAUDE.md", ".cursor/rules/project.mdc", ".windsurfrules");

private final String agentContextPath;

/**
* Constructor for creating a new Challenge73 object.
*
* @param agentContextPath directory in the dev container that holds the agent instruction files.
*/
public Challenge73(@Value("${AGENT_CONTEXT_PATH}") String agentContextPath) {
this.agentContextPath = agentContextPath;
}

@Override
public String getAnswer() {
var basePath = Path.of(agentContextPath);
for (var fileName : AGENT_CONTEXT_FILES) {
var file = basePath.resolve(fileName);
try {
var content = Files.readString(file, StandardCharsets.UTF_8);
var matcher = TOKEN_PATTERN.matcher(content);
if (matcher.find()) {
return matcher.group(1);
}
} catch (IOException e) {
log.warn("Could not read agent context file {} of challenge 73", file, e);
}
}
log.warn("Could not find the staging token in the agent context files of challenge 73");
return FILE_MOUNT_ERROR;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
Expand Down Expand Up @@ -32,26 +33,19 @@ public LlamaChatController(ObjectMapper objectMapper, @Value("${LLAMAURL}") Stri
produces = MediaType.APPLICATION_JSON_VALUE)
public ChatResponse chat(@RequestBody ChatRequest request) throws Exception {

var llamaRequest =
"""
{
"messages": [
{
"role": "user",
"content": %s
}
],
"temperature": 0.1,
"max_tokens": 128
}
"""
.formatted(objectMapper.writeValueAsString(request.message()));
ObjectNode llamaRequest = objectMapper.createObjectNode();
llamaRequest.put("temperature", 0.1);
llamaRequest.put("max_tokens", 128);
var userMessage = llamaRequest.putArray("messages").addObject();
userMessage.put("role", "user");
userMessage.put("content", request.message());

var httpRequest =
HttpRequest.newBuilder()
.uri(URI.create(llamaUrl + "/v1/chat/completions"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(llamaRequest))
.POST(
HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(llamaRequest)))
.build();

var response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ management.endpoints.web.exposure.include=auditevents,info,health
chalenge_docker_mount_secret=/var/run/secrets2
BASTIONHOSTPATH=.ssh
PROJECTSPECPATH=./cursor/rules/project-specification.mdc
AGENT_CONTEXT_PATH=./.devcontainer
#---
spring.config.activate.on-profile=kubernetes-vault
wrongsecretvalue=wrongsecret
Expand Down
25 changes: 25 additions & 0 deletions src/main/resources/explanations/challenge73.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
=== Challenge 73: Find the Secret in the Agent Instruction Files

AI coding agents load instruction and context files from the repository on every
session: `AGENTS.md`, `CLAUDE.md`, `.cursor/rules/*.mdc`, `.windsurfrules` and
similar. Because these files are "just documentation", they rarely get the same
review as source code, yet they are read by a model, copied to every developer
machine and committed to version control.

This repository ships such instruction files with its Dev Container. They live
in the link:https://github.com/OWASP/wrongsecrets/tree/master/.devcontainer[`.devcontainer`]
folder:

- `.devcontainer/AGENTS.md`
- `.devcontainer/CLAUDE.md`
- `.devcontainer/.cursor/rules/project.mdc`
- `.devcontainer/.windsurfrules`

One of them inlines a shared credential so that every agent reuses the same
value "for convenience". Read the files and submit the exposed staging token.

[NOTE]
====
Nothing is encoded or encrypted here: this is exactly what a leaked agent
context file looks like in the wild.
====
15 changes: 15 additions & 0 deletions src/main/resources/explanations/challenge73_hint.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
The agent instruction files are shipped with the Dev Container in the
`.devcontainer` folder:

- `.devcontainer/AGENTS.md`
- `.devcontainer/CLAUDE.md`
- `.devcontainer/.cursor/rules/project.mdc`
- `.devcontainer/.windsurfrules`

Three of them only point at where the shared credential lives. One of them
inlines the value of `STAGING_DEPLOY_TOKEN` in a shell snippet. That value is
the answer.

If you are not using the Dev Container, you can also read the same files
straight from the repository in the link:https://github.com/OWASP/wrongsecrets/tree/master/.devcontainer[`.devcontainer`]
folder.
39 changes: 39 additions & 0 deletions src/main/resources/explanations/challenge73_reason.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
*Why you should never put a secret in an agent instruction file*

`AGENTS.md`, `CLAUDE.md`, `.cursor/rules/*.mdc`, `.windsurfrules` and the many
other agent context formats are loaded automatically by coding agents. That
makes them convenient, and that convenience is exactly what turns them into a
secret management problem:

- They are committed to the repository, so the secret is in the git history
forever and has to be rotated the moment it appears.
- They are copied to every machine that checks out the repository, including
forks, CI runners and personal laptops.
- They are loaded into the model context, so the value can end up in chat
transcripts, telemetry and logs owned by third parties.
- They usually carry a *shared* credential, which is rarely scoped down and
even more rarely rotated.
- Many secret scanners treat documentation files as low priority, so the leak
survives review for a long time.

----
What to do instead:

- Keep credentials out of the instruction files and let the agent read them
from the environment, for example `$STAGING_DEPLOY_TOKEN`.
- Document *which* variable is required and *where* to obtain it, never the
value itself.
- Give every consumer a short-lived, least-privilege credential instead of one
shared token.
- Scan agent instruction files with a secret scanner in pre-commit and CI, just
like you scan source code.
- Treat any secret that ever appeared in agent context as compromised and
rotate it.
----

[NOTE]
====
Skills, rules, prompts, MCP server configurations and agent instruction files
are all code as far as your secret management is concerned. If you would not
hardcode a token in a `.java` file, do not hardcode it in a `CLAUDE.md` either.
====
15 changes: 14 additions & 1 deletion src/main/resources/wrong-secrets-configuration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1105,10 +1105,23 @@ configurations:
ctf:
enabled: true

- name: Challenge 73
short-name: "challenge-73"
sources:
- class-name: "org.owasp.wrongsecrets.challenges.docker.Challenge73"
explanation: "explanations/challenge73.adoc"
hint: "explanations/challenge73_hint.adoc"
reason: "explanations/challenge73_reason.adoc"
environments: *all_envs
difficulty: *easy
category: *ai
ctf:
enabled: true

- name: Challenge 74
short-name: "challenge-74"
sources:
- class-name: "org.owasp.wrongsecrets.challenges.k8s.llama.Challenge74"
- class-name: "org.owasp.wrongsecrets.challenges.kubernetes.llama.Challenge74"
explanation: "explanations/challenge74.adoc"
hint: "explanations/challenge74_hint.adoc"
reason: "explanations/challenge74_reason.adoc"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class Challenge73RegistrationTest {

private static final String EXPECTED_TOKEN = "Ag3nt_C0nt3xt_L3ak3d_T0k3n!";

@Autowired private Challenges challenges;

@Test
void challenge73ShouldBeRegisteredAndDiscoverable() {
var definition = challenges.findByShortName("challenge-73");

assertThat(definition).isPresent();
assertThat(challenges.getChallenge(definition.get())).hasSize(1);
assertThat(challenges.getChallenge(definition.get()).getFirst())
.isInstanceOf(Challenge73.class);
}

@Test
void registeredChallenge73ShouldExposeTheLeakedToken() {
var definition = challenges.findByShortName("challenge-73").orElseThrow();

var challenge = challenges.getChallenge(definition).getFirst();

assertThat(challenge.spoiler().solution()).isEqualTo(EXPECTED_TOKEN);
assertThat(challenge.answerCorrect(challenge.spoiler().solution())).isTrue();
}
}
Loading
Loading