Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .lycheeignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,8 @@ https://www.jcchouinard.com/wget/
# Railway deploy template links return 404 to link checkers
https://railway.com/deploy/*

# Staging internal URL from challenge 72 transcript is not reachable from CI
https://staging.internal.wrongsecrets.example.com/

# Slack gives 403, but channels exists for a few years now
https://owasp.slack.com/*
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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 leaked inside a real AI coding-agent transcript. The transcript
* captures a Codex session where the agent reads a staging configuration file containing a deploy
* token, exposing it in the session output. This illustrates how AI coding-agent transcripts can
* inadvertently retain sensitive information that may later be committed, shared, or exposed.
*/
@Slf4j
@Component
public class Challenge72 extends FixedAnswerChallenge {

private static final Pattern DEPLOY_TOKEN_PATTERN =
Pattern.compile("DEPLOY_TOKEN=([A-Za-z0-9_]+)");

private final Resource transcriptFile;

public Challenge72(
@Value("classpath:challenges/challenge-72/codex-session-transcript.md")
Resource transcriptFile) {
this.transcriptFile = transcriptFile;
}

@Override
public String getAnswer() {
try {
var transcriptContent = transcriptFile.getContentAsString(StandardCharsets.UTF_8);
var matcher = DEPLOY_TOKEN_PATTERN.matcher(transcriptContent);
if (!matcher.find()) {
log.warn("Could not find the deploy token in the Codex transcript of challenge 72");
return FILE_MOUNT_ERROR;
}
return matcher.group(1);
} catch (IOException e) {
log.warn("Exception while reading the Codex transcript of challenge 72", 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 Codex session transcript of challenge 72 straight from the resource folder, so
* participants can read the transcript the same way an analyst would when reviewing agent output.
*/
@Slf4j
@RestController
public class Challenge72Controller {

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

private final Resource transcriptFile;

public Challenge72Controller(
@Value("classpath:challenges/challenge-72/codex-session-transcript.md")
Resource transcriptFile) {
this.transcriptFile = transcriptFile;
}

/** Returns the raw Codex session transcript for challenge 72. */
@GetMapping("/challenges/challenge-72/codex-session-transcript.md")
public ResponseEntity<String> codexTranscript() {
try {
return ResponseEntity.ok()
.contentType(MARKDOWN)
.body(transcriptFile.getContentAsString(StandardCharsets.UTF_8));
} catch (IOException e) {
log.warn("Unable to serve the Codex transcript of challenge 72", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}
38 changes: 38 additions & 0 deletions src/main/resources/challenges/challenge-72/challenge-72.snippet
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<div id="codex-transcript-container" style="border: 1px solid #ccc; border-radius: 8px; padding: 20px; margin: 20px; background-color: #f9f9f9;">

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.

can you update the css file to include this div for rendering, similar to the other challenge divs?

<h4>🤖 Codex session transcript</h4>
<p>This application ships a Codex session transcript where a developer asked the agent to investigate a failing staging deployment. The transcript captures the agent reading configuration files and outputting their contents — including a deploy token.</p>

<div class="skill-warning" style="border: 1px solid #ffeaa7; border-radius: 6px; padding: 15px; margin: 15px 0;">
<p>Fetch the transcript:</p>
<pre class="skill-code"
style="padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 13px;"
th:text="'curl -sO ' + ${httpServletRequest.scheme} + '://' + ${httpServletRequest.serverName} + (${httpServletRequest.serverPort == 80 || httpServletRequest.serverPort == 443 ? '' : ':' + httpServletRequest.serverPort}) + '/challenges/challenge-72/codex-session-transcript.md'">curl -sO http://localhost:8080/challenges/challenge-72/codex-session-transcript.md</pre>

<p style="margin-top: 12px;">…or read it right here:</p>
<button class="btn btn-warning btn-sm skill-btn" onclick="loadTranscript()">▶ Show transcript</button>
<a class="btn btn-secondary btn-sm skill-btn" href="/challenges/challenge-72/codex-session-transcript.md" target="_blank" rel="noopener">Open in new tab</a>
<pre id="transcript-output" class="transcript-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 agent read a staging config file and echoed its contents — including a deploy token. Find that token in the transcript output.</small></p>
</div>
</div>

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

<script>
function loadTranscript() {
const out = document.getElementById('transcript-output');
out.style.display = 'block';
out.textContent = 'Loading…';
fetch('/challenges/challenge-72/codex-session-transcript.md')
.then(function(r) { return r.text(); })
.then(function(text) { out.textContent = text; })
.catch(function(err) { out.textContent = 'Failed to load the transcript: ' + (err.message || err); });
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
OpenAI Codex v0.147.0
--------
workdir: C:\Users\Lenovo\AppData\Local\Temp\opencode\codex-session
model: gpt-5.5
provider: openai
approval: never
sandbox: read-only
reasoning effort: medium
reasoning summaries: none
session id: 01a084fb-dc06-7c21-81bf-3c61061c424f
--------
user
The staging deployment is failing with 401 Unauthorized. The token was verified yesterday. Please investigate the configuration and deployment code to find the issue. Read the .env.staging file and the deploy.js file to understand what's happening.
codex
I'll inspect the staging environment values and the deployment script first, then trace how the token is passed so we can separate a bad secret from a config/code issue.
exec
"C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe" -Command 'Get-ChildItem -Force' in C:\Users\Lenovo\AppData\Local\Temp\opencode\codex-session
exec
"C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe" -Command 'Get-Content -Raw deploy.js' in C:\Users\Lenovo\AppData\Local\Temp\opencode\codex-session
exec
"C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe" -Command 'Get-Content -Raw .env.staging' in C:\Users\Lenovo\AppData\Local\Temp\opencode\codex-session
succeeded in 438ms:
# Staging environment configuration
# DO NOT COMMIT - shared team token

API_BASE_URL=https://staging.internal.wrongsecrets.example.com
DEPLOY_TOKEN=DevS3cret_T0k3n_2024_xK9mP
LOG_LEVEL=debug

succeeded in 404ms:
const axios = require('axios');
const fs = require('fs');
const path = require('path');

async function deploy() {
try {
const configPath = path.join(__dirname, '.env.staging');
const config = fs.readFileSync(configPath, 'utf8');

const configObj = {};
config.split('\n').forEach(line => {
if (line && !line.startsWith('#')) {
const [key, value] = line.split('=');
configObj[key.trim()] = value.trim();
}
});

console.log('Deploying to:', configObj.API_BASE_URL);

const response = await axios.post(configObj.API_BASE_URL + '/deploy', {
branch: 'main',
timestamp: Date.now()
}, {
headers: {
'Authorization': `Bearer ${configObj.DEPLOY_TOKEN}`,
'Content-Type': 'application/json'
}
});

console.log('Deploy successful:', response.data);
return response.data;
} catch (error) {
console.error('Deploy failed:', error.message);
throw error;
}
}

deploy();

succeeded in 3204ms:


Directory: C:\Users\Lenovo\AppData\Local\Temp\opencode\codex-session


Mode LastWriteTime Length Name
---- ------------- ------
d--h-- 09-09-2026 12:34 .git
-a---- 09-09-2026 12:32 192 .env.staging
-a---- 09-09-2026 12:32 986 deploy.js
-a---- 09-09-2026 12:32 286 package.json
-a---- 09-09-2026 12:33 334 README.md


codex
The script is loading `.env.staging` directly and sending `DEPLOY_TOKEN` as a Bearer token. One suspicious detail already stands out: the staging base URL includes `wrongsecrets`, which may be a misconfigured endpoint rather than the verified token itself. I'll check the surrounding repo docs/config for the expected staging host or deploy contract.
exec
"C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe" -Command 'Get-Content -Raw package.json' in C:\Users\Lenovo\AppData\Local\Temp\opencode\codex-session
exec
"C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe" -Command 'Get-Content -Raw README.md' in C:\Users\Lenovo\AppData\Local\Temp\opencode\codex-session
exec
"C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe" -Command 'rg -n "staging|API_BASE_URL|DEPLOY_TOKEN|deploy|wrongsecrets|Authorization|Bearer" -S .' in C:\Users\Lenovo\AppData\Local\Temp\opencode\codex-session
succeeded in 397ms:
{
"name": "staging-deploy-tool",
"version": "1.0.0",
"description": "Internal deployment utility",
"main": "deploy.js",
"scripts": {
"deploy": "node deploy.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"axios": "^1.6.0"
}
}

succeeded in 414ms:
# Staging Deploy Tool

Internal tool for deploying to staging environment.

## Bug Report

The deployment is failing with a 401 Unauthorized error, even though the token was verified to be correct in the password manager yesterday.

Please investigate why the deployment is failing and identify if there are any configuration issues.

succeeded in 2015ms:
.\README.md:3:Internal tool for deploying to staging environment.
.\README.md:7:The deployment is failing with a 401 Unauthorized error, even though the token was verified to be correct in the password manager yesterday.
.\README.md:9:Please investigate why the deployment is failing and identify if there are any configuration issues.
.\package.json:2: "name": "staging-deploy-tool",
.\package.json:4: "description": "Internal deployment utility",
.\package.json:5: "main": "deploy.js",
.\package.json:7: "deploy": "node deploy.js",
.\deploy.js:5:async function deploy() {
.\deploy.js:7: const configPath = path.join(__dirname, '.env.staging');
.\deploy.js:18: console.log('Deploying to:', configObj.API_BASE_URL);
.\deploy.js:20: const response = await axios.post(configObj.API_BASE_URL + '/deploy', {
.\deploy.js:25: 'Authorization': `Bearer ${configObj.DEPLOY_TOKEN}`,
.\deploy.js:38:deploy();

codex
There are no other config references in the working tree, so I'm checking git metadata next. If the token was verified externally, the most likely causes are an endpoint mismatch, config parsing edge case, or a recent change in how auth is assembled.
exec
"C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe" -Command 'git log --oneline --decorate -5' in C:\Users\Lenovo\AppData\Local\Temp\opencode\codex-session
exec
"C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe" -Command 'git status --short' in C:\Users\Lenovo\AppData\Local\Temp\opencode\codex-session
exec
"C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe" -Command 'git show --stat --oneline --decorate --name-only HEAD' in C:\Users\Lenovo\AppData\Local\Temp\opencode\codex-session
2026-09-09T07:05:36.210923Z ERROR codex_core::tools::router: error=Exit code: 1
Wall time: 1.3 seconds
Output:
fatal: your current branch 'master' does not have any commits yet

exited 1 in 1312ms:
fatal: your current branch 'master' does not have any commits yet

2026-09-09T07:05:36.241898Z ERROR codex_core::tools::router: error=Exit code: 1
Wall time: 1.3 seconds
Output:
fatal: ambiguous argument 'HEAD': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'

succeeded in 1413ms:
?? .env.staging
?? README.md
?? deploy.js
?? package.json
warning: unable to access 'C:\Users\Lenovo/.config/git/ignore': Permission denied

exited 1 in 1254ms:
fatal: ambiguous argument 'HEAD': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'

codex
The repo has no commits, so there's no history to compare against. I'll grab line numbers for the two relevant files and then summarize the failure path without exposing the full secret value.
exec
"C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe" -Command '$i=1; Get-Content deploy.js | ForEach-Object { '"'{0,4}: {1}' -f "'$i, $_; $i++ }' in C:\Users\Lenovo\AppData\Local\Temp\opencode\codex-session
exec
"C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe" -Command '$i=1; Get-Content .env.staging | ForEach-Object { '"'{0,4}: {1}' -f "'$i, $_; $i++ }' in C:\Users\Lenovo\AppData\Local\Temp\opencode\codex-session
succeeded in 1257ms:
1: # Staging environment configuration
2: # DO NOT COMMIT - shared team token
3:
4: API_BASE_URL=https://staging.internal.wrongsecrets.example.com
5: DEPLOY_TOKEN=DevS3cret_T0k3n_2024_xK9mP
6: LOG_LEVEL=debug

succeeded in 1298ms:
1: const axios = require('axios');
2: const fs = require('fs');
3: const path = require('path');
4:
5: async function deploy() {
6: try {
7: const configPath = path.join(__dirname, '.env.staging');
8: const config = fs.readFileSync(configPath, 'utf8');
9:
10: const configObj = {};
11: config.split('\n').forEach(line => {
12: if (line && !line.startsWith('#')) {
13: const [key, value] = line.split('=');
14: configObj[key.trim()] = value.trim();
15: }
16: });
17:
18: console.log('Deploying to:', configObj.API_BASE_URL);
19:
20: const response = await axios.post(configObj.API_BASE_URL + '/deploy', {
21: branch: 'main',
22: timestamp: Date.now()
23: }, {
24: headers: {
25: 'Authorization': `Bearer ${configObj.DEPLOY_TOKEN}`,
26: 'Content-Type': 'application/json'
27: }
28: });
29:
30: console.log('Deploy successful:', response.data);
31: return response.data;
32: } catch (error) {
33: console.error('Deploy failed:', error.message);
34: throw error;
35: }
36: }
37:
38: deploy();

codex
The likely issue is configuration, not the token.

`.env.staging` points `API_BASE_URL` at:

`https://staging.internal.wrongsecrets.example.com`

Then [deploy.js](/C:/Users/Lenovo/AppData/Local/Temp/opencode/codex-session/deploy.js:20) posts to that host's `/deploy` endpoint with the verified `DEPLOY_TOKEN` as `Authorization: Bearer ...`.

If the token was verified yesterday, a `401 Unauthorized` is consistent with sending the correct token to the wrong service/environment. The hostname strongly suggests the staging deploy is using the wrong secrets/config target: `wrongsecrets`.

Secondary issue: [deploy.js](/C:/Users/Lenovo/AppData/Local/Temp/opencode/codex-session/deploy.js:13) parses env lines with `line.split('=')`, which will corrupt values containing `=`. That is not causing this exact token to fail, but it is brittle and should be fixed.

I could not patch the files because the workspace is read-only, but the immediate fix is to correct `API_BASE_URL` in `.env.staging` to the real staging deployment API host. Then harden the parser or use `dotenv` so future tokens/URLs with `=` are handled correctly.
14 changes: 14 additions & 0 deletions src/main/resources/explanations/challenge72.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
=== Challenge 72: Find the Secret in the AI Coding-Agent Transcript

AI coding agents like Codex, Claude Code, and Cursor read files, run commands, and produce detailed session transcripts. When a developer asks an agent to debug a deployment issue, the agent may read configuration files containing secrets and echo those values into its session output.

This application ships a Codex session transcript where a developer asked the agent to investigate a failing staging deployment. The transcript captures the agent reading a `.env.staging` file and outputting its contents — including the deploy token — as part of normal debugging workflow.

Download the transcript from link:/challenges/challenge-72/codex-session-transcript.md[`/challenges/challenge-72/codex-session-transcript.md`] and find the staging deploy token that was exposed during the session.

The source is also available at link:https://github.com/OWASP/wrongsecrets/blob/master/src/main/resources/challenges/challenge-72/codex-session-transcript.md[`src/main/resources/challenges/challenge-72/codex-session-transcript.md`].

[NOTE]
====
The token appears naturally in the transcript output — the agent did not intend to leak it, it simply read a file as part of its debugging process.
====
Loading
Loading