Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
bfe68fb
Initial plan
Copilot Jul 29, 2025
7a4d274
Implement Challenge 57: Database Connection String Exposure through E…
Copilot Jul 29, 2025
372f123
Fix formatting issues in Challenge 57 files
Copilot Jul 29, 2025
3b94656
Merge branch 'master' into copilot/fix-2122
commjoen Jul 30, 2025
c9c4725
Merge branch 'master' into copilot/fix-2122
commjoen Aug 2, 2025
cc5f015
Fix GitHub Pages workflow artifact naming conflict
Copilot Aug 2, 2025
161cec1
Implement Challenge 58 and update Challenge 57 with complete GitHub P…
Copilot Aug 2, 2025
e2449d3
Fix spotless formatting violations and Python script syntax warning
Copilot Aug 3, 2025
9cf7246
Fix MediaType constant issue in Challenge57Controller
Copilot Aug 3, 2025
7252d92
Merge branch 'master' into copilot/fix-2122
commjoen Aug 3, 2025
cc8b721
Fix workflow failures: Black formatting and SpotBugs violation
Copilot Aug 3, 2025
e4f177c
Fix HTML and adoc formatting issues and add ZAP whitelist rule
Copilot Aug 3, 2025
5b83209
Merge master and resolve conflicts - keep only Challenge 57 (Database…
Copilot Aug 22, 2025
d8e2623
Clean up remaining Challenge58 files after merge
Copilot Aug 22, 2025
7bb7d6a
Fix Challenge structure: Challenge 57 as LLM, Challenge 58 as databas…
Copilot Aug 22, 2025
11b2faa
Restore Challenge 57 LLM files from master and fix Challenge 58 forma…
Copilot Aug 23, 2025
a0b85b8
Update preview generation script to include Challenge 58 as latest ch…
Copilot Aug 23, 2025
134b5a1
Fix Challenge 58 missing files and Black formatting issues
Copilot Aug 23, 2025
7f05a3e
Merge branch 'master' into copilot/fix-2122
commjoen Aug 23, 2025
c907dad
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Aug 23, 2025
57aef45
Merge branch 'master' into copilot/fix-2122
commjoen Aug 23, 2025
e066120
Merge branch 'master' into copilot/fix-2122
commjoen Aug 23, 2025
6abff4f
undo copilot issues: have git ignore removed and add the right snippets
commjoen Aug 24, 2025
57b4655
extend explanation
commjoen Aug 27, 2025
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
274 changes: 270 additions & 4 deletions .github/scripts/generate_thymeleaf_previews.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,103 @@ def replace_th_attr(self, content):
content = re.sub(r'th:attr="[^"]*"', "", content)
return content

def add_static_assets_challenge58(self, content):
"""Add embedded CSS and JS for Challenge 58 static preview."""
if "<head>" in content:
head_additions = f"""
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OWASP WrongSecrets - Challenge 58 Preview</title>
<style>
{self.embedded_css}
.preview-banner {{
background: #f8f9fa;
border: 1px solid #dee2e6;
padding: 10px 15px;
margin-bottom: 20px;
border-radius: 5px;
}}
.preview-banner .alert-heading {{
color: #0c5460;
font-size: 1.1em;
margin-bottom: 5px;
}}
.solved {{ background-color: #d4edda; }}

/* Challenge 58 specific styles */
.demo-section {{
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 6px;
padding: 15px;
margin: 15px 0;
}}

.demo-section .btn-warning {{
background-color: #ffc107;
border-color: #ffc107;
color: #212529;
text-decoration: none;
display: inline-block;
padding: 8px 16px;
border-radius: 4px;
border: 1px solid transparent;
font-weight: 400;
text-align: center;
vertical-align: middle;
cursor: pointer;
font-size: 1rem;
line-height: 1.5;
margin-top: 10px;
}}

.demo-section .btn-warning:hover {{
background-color: #e0a800;
border-color: #d39e00;
}}

/* Challenge explanation sections */
.challenge-content {{
margin-bottom: 30px;
}}
.explanation-content, .hint-content, .reason-content {{
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 6px;
padding: 15px;
margin-bottom: 20px;
}}
.explanation-content h3, .hint-content h3, .reason-content h3 {{
color: #495057;
margin-top: 0;
}}
.explanation-content ul, .hint-content ul, .reason-content ul {{
margin-bottom: 10px;
}}
.explanation-content li, .hint-content li, .reason-content li {{
margin-bottom: 5px;
}}
</style>"""
content = content.replace("<head>", f"<head>{head_additions}")

# Add preview banner for Challenge 58
banner = f"""
<div class="preview-banner">
<div class="alert-heading">🗄️ Challenge 58 - Database Connection String Exposure (PR #{self.pr_number})</div>
<small>This is a live preview of Challenge 58 demonstrating how database credentials leak through error messages. Click the demo button to see the vulnerable endpoint in action!</small>
</div>"""

if '<div class="container"' in content:
content = content.replace(
'<div class="container"', f'<div class="container">{banner}'
)
elif "<body>" in content:
content = content.replace(
"<body>", f'<body><div class="container">{banner}</div>'
)

return content

def add_static_assets(self, content, template_name):
"""Add embedded CSS and JS for the static preview."""
if "<head>" in content:
Expand Down Expand Up @@ -632,6 +729,120 @@ def generate_stats_page(self):

return content

def generate_challenge58_page(self):
"""Generate Challenge 58 (Database Connection String Exposure) page with embedded content."""
template_path = self.templates_dir / "challenge.html"

if not template_path.exists():
print(f"Warning: Template {template_path} not found")
return self.generate_fallback_challenge58()

with open(template_path, "r", encoding="utf-8") as f:
content = f.read()

# Mock Challenge 58 data
mock_challenge = {
"name": "Challenge 58: Database Connection String Exposure",
"stars": "⭐⭐⭐",
"tech": "LOGGING",
"explanation": "challenge58.adoc",
"hint": "challenge58_hint.adoc",
"reason": "challenge58_reason.adoc",
"link": "/challenge/challenge-58",
}

# Replace challenge-specific Thymeleaf content
content = re.sub(
r'<span th:text="\$\{challenge\.name\}"[^>]*>[^<]*</span>',
f'<span data-cy="challenge-title">{mock_challenge["name"]}</span>',
content,
)
content = re.sub(
r'<span[^>]*th:text="\$\{challenge\.stars\}"[^>]*>[^<]*</span>',
f'<span>{mock_challenge["stars"]}</span>',
content,
)
content = re.sub(
r'<strong th:text="\$\{challenge\.tech\}"[^>]*>[^<]*</strong>',
f'<strong>{mock_challenge["tech"]}</strong>',
content,
)
content = re.sub(
r'<span th:text="\'Welcome to challenge \'\s*\+\s*\$\{challenge\.name\}\s*\+\s*\'\.\'"></span>',
f'<span>Welcome to challenge {mock_challenge["name"]}.</span>',
content,
)

# Replace the explanation section with Challenge 58 content
explanation_pattern = (
r'<div th:replace="~\{doc:__\$\{challenge\.explanation\}__\}"></div>'
)

# Load actual Challenge 58 content from AsciiDoc files
explanation_content = self.load_adoc_content("challenge58.adoc")
hint_content = self.load_adoc_content("challenge58_hint.adoc")
reason_content = self.load_adoc_content("challenge58_reason.adoc")

challenge58_explanation = f"""
<div class="challenge-explanation">
<div class="challenge-content">
<h4>📖 Challenge Explanation</h4>
<div class="explanation-content">
{explanation_content}
</div>

<h4>💡 Hints</h4>
<div class="hint-content">
{hint_content}
</div>

<h4>🧠 Reasoning</h4>
<div class="reason-content">
{reason_content}
</div>
</div>

<div class="challenge-demo">
<h4>🔗 Database Connection Error Demo</h4>
<div class="demo-section">
<p><strong>Try the vulnerable endpoint:</strong></p>
<a href="/error-demo/database-connection" class="btn btn-warning">
🚨 Trigger Database Connection Error
</a>
<p><small class="text-muted">This endpoint simulates a database connection failure that exposes the connection string with embedded credentials.</small></p>
</div>
</div>
</div>
"""
content = re.sub(
explanation_pattern, lambda m: challenge58_explanation, content
)

# Process the template
content = self.process_thymeleaf_syntax(content, "challenge58")

# Ensure we have a proper HTML structure with head
if "<head>" not in content:
# Add basic HTML structure
content = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OWASP WrongSecrets - Challenge 58</title>
</head>
{content}
</html>"""

# Add embedded CSS and styling for Challenge 58
content = self.add_static_assets_challenge58(content)

# Add navigation
nav = self.generate_navigation_html()
content = content.replace("<body>", f"<body>{nav}")

return content

def generate_challenge57_page(self):
"""Generate Challenge 57 (LLM Challenge) page with embedded content."""
template_path = self.templates_dir / "challenge.html"
Expand Down Expand Up @@ -899,6 +1110,57 @@ def generate_fallback_challenge57_snippet(self):
</script>
"""

def generate_fallback_challenge58(self):
"""Generate a fallback Challenge 58 page if template is missing."""
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OWASP WrongSecrets - Challenge 58</title>
<style>
{self.embedded_css}
</style>
</head>
<body>
{self.generate_navigation_html()}
<div class="container mt-4">
<div class="preview-banner">
<div class="alert-heading">🗄️ Challenge 58 - Database Connection String Exposure (PR #{self.pr_number})</div>
<small>This is a live preview of Challenge 58 demonstrating how database credentials leak through error messages.</small>
</div>

<h1>Challenge 58: Database Connection String Exposure ⭐⭐⭐</h1>
<p>Welcome to Challenge 58: Database Connection String Exposure.</p>

<div class="alert alert-primary" role="alert">
<h6 class="alert-heading">🔍 Your Task</h6>
<p class="mb-2">Find the database password that gets exposed when the application fails to connect to the database.</p>
<p class="mb-0">💡 <strong>Visit:</strong> The <code>/error-demo/database-connection</code> endpoint to trigger the error.</p>
</div>

<div class="demo-section">
<h4>🔗 Database Connection Error Demo</h4>
<p><strong>Try the vulnerable endpoint:</strong></p>
<a href="/error-demo/database-connection" class="btn btn-warning">
🚨 Trigger Database Connection Error
</a>
<p><small class="text-muted">This endpoint simulates a database connection failure that exposes the connection string with embedded credentials.</small></p>
</div>

<form>
<div class="mb-3">
<label for="answerfield" class="form-label"><strong>🔑 Enter the database password you found:</strong></label>
<input type="text" class="form-control" id="answerfield" placeholder="Type the password here..."/>
<small class="form-text text-muted">💡 Tip: Look for the password in the database connection error message.</small>
</div>
<button class="btn btn-primary" type="button">🚀 Submit Answer</button>
<button class="btn btn-secondary" type="button" onclick="document.getElementById('answerfield').value='';">🗑️ Clear</button>
</form>
</div>
</body>
</html>"""

def generate_fallback_challenge57(self):
"""Generate a fallback Challenge 57 page if template is missing."""
return f"""<!DOCTYPE html>
Expand Down Expand Up @@ -1069,7 +1331,7 @@ def generate_fallback_challenge(self):
</html>"""

def generate_all_pages(self):
"""Generate all static pages with Challenge 57 as the featured challenge."""
"""Generate all static pages with Challenge 58 as the featured latest challenge."""
# Create pages directory
pages_dir = self.static_dir / f"pr-{self.pr_number}" / "pages"
pages_dir.mkdir(parents=True, exist_ok=True)
Expand All @@ -1078,8 +1340,9 @@ def generate_all_pages(self):
"welcome.html": self.generate_welcome_page(),
"about.html": self.generate_about_page(),
"stats.html": self.generate_stats_page(),
"challenge-57.html": self.generate_challenge57_page(), # Always render Challenge 57
"challenge-example.html": self.generate_challenge57_page(), # Use Challenge 57 as the example too
"challenge-57.html": self.generate_challenge57_page(), # LLM Challenge (AI category)
"challenge-58.html": self.generate_challenge58_page(), # Database Challenge (Latest)
"challenge-example.html": self.generate_challenge58_page(), # Use Challenge 58 as the latest example
}

for filename, content in pages.items():
Expand All @@ -1089,7 +1352,10 @@ def generate_all_pages(self):
print(f"Generated {filename}")

print(f"Generated {len(pages)} static pages in {pages_dir}")
print(f"✅ Challenge 57 (LLM Security) is featured as the latest challenge")
print(
f"✅ Challenge 57 (LLM Security) and Challenge 58 (Database Connection String Exposure) are both available"
)
print(f"✅ Challenge 58 is featured as the latest challenge")
return pages_dir


Expand Down
2 changes: 1 addition & 1 deletion .github/scripts/remove_pr_from_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def main():

# Remove the PR card for this specific PR number
card_pattern = (
f'<div class="pr-card"[^>]*data-pr="{pr_number}"[^>]*>.*?</div>\s*</div>'
rf'<div class="pr-card"[^>]*data-pr="{pr_number}"[^>]*>.*?</div>\s*</div>'
)
updated_content = re.sub(card_pattern, "", content, flags=re.DOTALL)

Expand Down
18 changes: 9 additions & 9 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 57 challenges?
Can you solve all the 58 challenges?

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

Expand Down Expand Up @@ -127,16 +127,16 @@ Not sure which setup is right for you? Here's a quick guide:

| **I want to...** | **Recommended Setup** | **Challenges Available** |
|------------------|----------------------|--------------------------|
| Try it quickly online | [Container running on Heroku](https://www.wrongsecrets.com/) | Basic challenges (1-4, 8, 12-32, 34-43, 49-52, 54-57) |
| Try it quickly online | [Container running on Heroku](https://www.wrongsecrets.com/) | Basic challenges (1-4, 8, 12-32, 34-43, 49-52, 54-58) |
| 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-57) |
| Practice with cloud secrets | [Cloud Challenges](#cloud-challenges) | All challenges (1-57) |
| Learn Kubernetes secrets | [K8s/Minikube Setup](#basic-k8s-exercise) | Kubernetes challenges (1-6, 8, 12-43, 48-58) |
| Practice with cloud secrets | [Cloud Challenges](#cloud-challenges) | All challenges (1-87) |
| Run a workshop/CTF | [CTF Setup](#ctf) | Customizable challenge sets |
| Contribute to the project | [Development Setup](#notes-on-development) | All challenges + development tools |

## Basic docker exercises

_Can be used for challenges 1-4, 8, 12-32, 34, 35-43, 49-52, 54-57_
_Can be used for challenges 1-4, 8, 12-32, 34, 35-43, 49-52, 54-58_

For the basic docker exercises you currently require:

Expand Down Expand Up @@ -208,7 +208,7 @@ Now you can try to find the secrets by means of solving the challenge offered at
- [localhost:8080/challenge/challenge-55](http://localhost:8080/challenge/challenge-55)
- [localhost:8080/challenge/challenge-56](http://localhost:8080/challenge/challenge-56)
- [localhost:8080/challenge/challenge-57](http://localhost:8080/challenge/challenge-57)

- [localhost:8080/challenge/challenge-58](http://localhost:8080/challenge/challenge-58)
</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 Expand Up @@ -237,7 +237,7 @@ If you want to host WrongSecrets on Railway, you can do so by deploying [this on

## Basic K8s exercise

_Can be used for challenges 1-6, 8, 12-43, 48-57_
_Can be used for challenges 1-6, 8, 12-43, 48-58_

### Minikube based

Expand Down Expand Up @@ -314,7 +314,7 @@ now you can use the provided IP address and port to further play with the K8s va

## Vault exercises with minikube

_Can be used for challenges 1-8, 12-57_
_Can be used for challenges 1-8, 12-58_
Make sure you have the following installed:

- minikube with docker (or comment out line 8 and work at your own k8s setup),
Expand All @@ -332,7 +332,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-57_
_Can be used for challenges 1-58_

**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
Expand Down
1 change: 1 addition & 0 deletions config/zap/rule-config.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading
Loading