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
6 changes: 4 additions & 2 deletions 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 67 challenges?
Can you solve all the 69 challenges?

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

Expand Down Expand Up @@ -161,7 +161,7 @@ docker run -p 8080:8080 -p 8090:8090 ghcr.io/owasp/wrongsecrets/wrongsecrets-mas
⚠️ **Warning**: This is a development version built from the latest master branch and may contain experimental features or instabilities.

**📝 Note on Ports:**
- Port **8080**: Main application (challenges 0-66)
- Port **8080**: Main application (challenges 0-68)
- Port **8090**: MCP server (required for Challenge 60)

**📝 Note on Challenge 62 (Google Drive MCP):**
Expand Down Expand Up @@ -226,6 +226,8 @@ Now you can try to find the secrets by means of solving the challenge offered at
- [localhost:8080/challenge/challenge-64](http://localhost:8080/challenge/challenge-64)
- [localhost:8080/challenge/challenge-65](http://localhost:8080/challenge/challenge-65)
- [localhost:8080/challenge/challenge-66](http://localhost:8080/challenge/challenge-66)
- [localhost:8080/challenge/challenge-70](http://localhost:8080/challenge/challenge-70)
- [localhost:8080/challenge/challenge-71](http://localhost:8080/challenge/challenge-71)
</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,48 @@
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.util.regex.Pattern;
import lombok.extern.slf4j.Slf4j;
import org.owasp.wrongsecrets.challenges.FixedAnswerChallenge;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;

/**
* Challenge based on a secret that is hardcoded in a Cursor skill. The skill is shipped as a plain
* {@code SKILL.md} file, so the secret is readable for anybody who receives the skill.
*/
@Slf4j
@Component
public class Challenge70 extends FixedAnswerChallenge {

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

private final Resource skillFile;

public Challenge70(
@Value("classpath:challenges/challenge-70/cursor-skill/deploy-preview/SKILL.md")
Resource skillFile) {
this.skillFile = skillFile;
}

@Override
public String getAnswer() {
try {
var skillContent = skillFile.getContentAsString(StandardCharsets.UTF_8);
var matcher = DEPLOY_TOKEN_PATTERN.matcher(skillContent);
if (!matcher.find()) {
log.warn("Could not find the deploy token in the Cursor skill of challenge 70");
return FILE_MOUNT_ERROR;
}
return matcher.group(1);
} catch (IOException e) {
log.warn("Exception while reading the Cursor skill of challenge 70", e);
return FILE_MOUNT_ERROR;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.owasp.wrongsecrets.challenges.docker;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* Hosts the Cursor skill of challenge 70 straight from the resource folder, so participants can
* read the skill the same way an agent would.
*/
@Slf4j
@RestController
public class Challenge70Controller {

private static final MediaType MARKDOWN =
new MediaType("text", "markdown", StandardCharsets.UTF_8);

private final Resource skillFile;

public Challenge70Controller(
@Value("classpath:challenges/challenge-70/cursor-skill/deploy-preview/SKILL.md")
Resource skillFile) {
this.skillFile = skillFile;
}

/** Returns the raw {@code SKILL.md} of the {@code deploy-preview} Cursor skill. */
@GetMapping("/skills/cursor/deploy-preview/SKILL.md")
public ResponseEntity<String> cursorSkill() {
try {
return ResponseEntity.ok()
.contentType(MARKDOWN)
.body(skillFile.getContentAsString(StandardCharsets.UTF_8));
} catch (IOException e) {
log.warn("Unable to serve the Cursor skill of challenge 70", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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.util.Base64;
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.core.io.Resource;
import org.springframework.stereotype.Component;

/**
* Challenge based on a secret that is hardcoded in a Claude skill. The skill is distributed as a
* zip bundle, and the token does not sit in the {@code SKILL.md} itself but in one of the bundled
* scripts, base64 encoded to keep secret scanners quiet.
*/
@Slf4j
@Component
public class Challenge71 extends FixedAnswerChallenge {

private static final Pattern UPLOAD_TOKEN_PATTERN =
Pattern.compile("UPLOAD_TOKEN_B64\\s*=\\s*\"([^\"]+)\"");

private final Resource uploaderScript;

public Challenge71(
@Value(
"classpath:challenges/challenge-71/claude-skill/incident-reporter/scripts/upload_report.py")
Resource uploaderScript) {
this.uploaderScript = uploaderScript;
}

@Override
public String getAnswer() {
try {
var scriptContent = uploaderScript.getContentAsString(StandardCharsets.UTF_8);
var matcher = UPLOAD_TOKEN_PATTERN.matcher(scriptContent);
if (!matcher.find()) {
log.warn("Could not find the upload token in the Claude skill of challenge 71");
return FILE_MOUNT_ERROR;
}
return new String(Base64.getDecoder().decode(matcher.group(1)), StandardCharsets.UTF_8);
} catch (IOException e) {
log.warn("Exception while reading the Claude skill of challenge 71", e);
return FILE_MOUNT_ERROR;
} catch (IllegalArgumentException e) {
log.warn("The upload token in the Claude skill of challenge 71 is not valid base64", e);
return FILE_MOUNT_ERROR;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package org.owasp.wrongsecrets.challenges.docker;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* Hosts the Claude skill of challenge 71. The skill files live in the resource folder and are
* zipped on request, so participants download the same kind of bundle that is passed around when a
* skill is shared.
*/
@Slf4j
@RestController
public class Challenge71Controller {

static final String SKILL_ROOT = "challenges/challenge-71/claude-skill/";
private static final String BUNDLE_NAME = "incident-reporter.zip";
private static final MediaType ZIP = new MediaType("application", "zip");
// Fixed timestamp (2024-01-01T00:00:00Z) so the generated bundle is reproducible.
private static final long FIXED_ENTRY_TIME = 1704067200000L;

/** The files that make up the skill, named as they appear inside the bundle. */
private static final List<String> SKILL_FILES =
List.of(
"incident-reporter/SKILL.md",
"incident-reporter/references/runbook.md",
"incident-reporter/scripts/upload_report.py");

/** Returns the {@code incident-reporter} Claude skill as a downloadable zip bundle. */
@GetMapping("/skills/claude/incident-reporter.zip")
public ResponseEntity<byte[]> claudeSkillBundle() {
try {
return ResponseEntity.ok()
.contentType(ZIP)
.headers(
headers ->
headers.setContentDisposition(
ContentDisposition.attachment().filename(BUNDLE_NAME).build()))
.body(zipSkill());
} catch (IOException e) {
log.warn("Unable to package the Claude skill of challenge 71", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}

/** Zips the skill files from the resource folder, keeping their paths inside the bundle. */
byte[] zipSkill() throws IOException {
var bundle = new ByteArrayOutputStream();
try (var zip = new ZipOutputStream(bundle)) {
for (String name : SKILL_FILES) {
var entry = new ZipEntry(name);
entry.setTime(FIXED_ENTRY_TIME);
zip.putNextEntry(entry);
try (var content = new ClassPathResource(SKILL_ROOT + name).getInputStream()) {
content.transferTo(zip);
}
zip.closeEntry();
}
}
return bundle.toByteArray();
Comment on lines +57 to +70

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is super clever :D , it now behaves like a skill. but given the markdown files are readable/searchable on source it makes it a little too easy. Please export the skill and replace it.

}
}
36 changes: 36 additions & 0 deletions src/main/resources/challenges/challenge-70/challenge-70.snippet
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<div id="cursor-skill-container" style="border: 1px solid #ccc; border-radius: 8px; padding: 20px; margin: 20px; background-color: #f9f9f9;">
<h4>📄 Cursor skill: <code>deploy-preview</code></h4>
<p>This application ships a Cursor skill in its resource folder. The backend hosts it at <code>/skills/cursor/deploy-preview/SKILL.md</code>, which is exactly what an agent would read when the skill is installed.</p>

<div class="skill-warning" style="border: 1px solid #ffeaa7; border-radius: 6px; padding: 15px; margin: 15px 0;">
<p>Fetch the skill from the command line:</p>
<pre class="skill-code" style="padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 13px;">curl -s http://localhost:8080/skills/cursor/deploy-preview/SKILL.md</pre>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this work?

Suggested change
<pre class="skill-code" style="padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 13px;">curl -s http://localhost:8080/skills/cursor/deploy-preview/SKILL.md</pre>
<pre class="skill-code"
style="padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 13px;"
th:text="'curl -s ' + ${#httpServletRequest.scheme} + '://' + ${#httpServletRequest.serverName} + ':' + ${#httpServletRequest.serverPort} + '/skills/cursor/deploy-preview/SKILL.md'">
</pre>

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that makes sense. I’ll use the request’s scheme, host, and port so the curl command works regardless of where the application is running.


<p style="margin-top: 12px;">…or read it right here:</p>
<button class="btn btn-warning btn-sm skill-btn" onclick="loadCursorSkill()">▶ Show SKILL.md</button>
<a class="btn btn-secondary btn-sm skill-btn" href="/skills/cursor/deploy-preview/SKILL.md" target="_blank" rel="noopener">Open in new tab</a>
<pre id="cursor-skill-output" class="skill-output" style="display:none; padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 12px; margin-top: 8px; white-space: pre-wrap; word-break: break-word;"></pre>

<p style="margin-top: 12px;"><small class="skill-tip">💡 The author inlined the shared staging deploy token so the skill would be "self-contained". Submit that token.</small></p>
</div>
</div>

<style>
.skill-output {
background-color: #f8f9fa !important;
color: #212529 !important;
border: 1px solid #dee2e6 !important;
}
</style>

<script>
function loadCursorSkill() {
const out = document.getElementById('cursor-skill-output');
out.style.display = 'block';
out.textContent = 'Loading…';
fetch('/skills/cursor/deploy-preview/SKILL.md')
.then(function(r) { return r.text(); })
.then(function(text) { out.textContent = text; })
.catch(function(err) { out.textContent = 'Failed to load the skill file: ' + (err.message || err); });
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
name: deploy-preview
description: Deploy the branch that is currently checked out to the shared staging preview environment and report the preview URL back to the user.
---

# Deploy Preview

Use this skill when the user asks you to "deploy a preview", "push this to
staging", or "get me a preview URL" for the branch that is currently checked
out.

## When to use this skill

- The user wants a throwaway preview environment for the current branch.
- The user wants to refresh a preview environment that already exists.

Do **not** use this skill for production releases. Production deployments go
through the release pipeline and require a second approver.

## Prerequisites

The staging deploy API authenticates with a shared team token. Previously
everybody had to look this token up in the password manager, which slowed the
agent down, so the token is inlined here to keep the skill self-contained:

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

## Steps

1. Verify the working tree is clean and the branch is pushed:

```bash
git status --porcelain
git push --set-upstream origin "$(git rev-parse --abbrev-ref HEAD)"
```

2. Build the application:

```bash
./mvnw --batch-mode --no-transfer-progress package -DskipTests
```

3. Trigger the preview deployment:

```bash
curl --silent --show-error --request POST "$STAGING_DEPLOY_URL" \
--header "Authorization: Bearer $STAGING_DEPLOY_TOKEN" \
--header "Content-Type: application/json" \
--data "{\"branch\": \"$(git rev-parse --abbrev-ref HEAD)\"}"
```

4. Report the `preview_url` field from the response back to the user.

## Troubleshooting

- `401 Unauthorized`: the shared token was rotated. Ask the platform team for
the new value and update the `STAGING_DEPLOY_TOKEN` line above.
- `409 Conflict`: a preview for this branch is already being built. Wait for the
running deployment to finish and try again.
17 changes: 17 additions & 0 deletions src/main/resources/challenges/challenge-71/challenge-71.snippet
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<div id="claude-skill-container" style="border: 1px solid #ccc; border-radius: 8px; padding: 20px; margin: 20px; background-color: #f9f9f9;">
<h4>📦 Claude skill bundle: <code>incident-reporter</code></h4>
<p>The skill files live in this application's resource folder. The backend packages them into a zip bundle on request, which is how a Claude skill is shared and installed.</p>

<div class="skill-warning" style="border: 1px solid #ffeaa7; border-radius: 6px; padding: 15px; margin: 15px 0;">
<p>Step 1 — download and unpack the bundle:</p>
<pre class="skill-code" style="padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 13px;">curl -sO http://localhost:8080/skills/claude/incident-reporter.zip
unzip incident-reporter.zip
find incident-reporter -type f</pre>
<a class="btn btn-warning btn-sm skill-btn" href="/skills/claude/incident-reporter.zip">⬇ Download incident-reporter.zip</a>

<p style="margin-top: 12px;">Step 2 — the <code>SKILL.md</code> is clean. Look at what it tells the agent to run:</p>
<pre class="skill-code" style="padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 13px;">grep -r TOKEN incident-reporter/</pre>

<p style="margin-top: 12px;"><small class="skill-tip">💡 The token you find is not the answer yet — the author "hid" it from the secret scanner. Submit the value the uploader actually authenticates with.</small></p>
</div>
</div>
Binary file not shown.
Loading