Skip to content

Commit ace6c83

Browse files
authored
Merge pull request #2123 from OWASP/copilot/fix-2122
Add Challenge 58: Database Connection String Exposure
2 parents 17db6da + 57b4655 commit ace6c83

File tree

16 files changed

+747
-1323
lines changed

16 files changed

+747
-1323
lines changed

.github/scripts/generate_thymeleaf_previews.py

Lines changed: 270 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,103 @@ def replace_th_attr(self, content):
431431
content = re.sub(r'th:attr="[^"]*"', "", content)
432432
return content
433433

434+
def add_static_assets_challenge58(self, content):
435+
"""Add embedded CSS and JS for Challenge 58 static preview."""
436+
if "<head>" in content:
437+
head_additions = f"""
438+
<meta charset="UTF-8">
439+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
440+
<title>OWASP WrongSecrets - Challenge 58 Preview</title>
441+
<style>
442+
{self.embedded_css}
443+
.preview-banner {{
444+
background: #f8f9fa;
445+
border: 1px solid #dee2e6;
446+
padding: 10px 15px;
447+
margin-bottom: 20px;
448+
border-radius: 5px;
449+
}}
450+
.preview-banner .alert-heading {{
451+
color: #0c5460;
452+
font-size: 1.1em;
453+
margin-bottom: 5px;
454+
}}
455+
.solved {{ background-color: #d4edda; }}
456+
457+
/* Challenge 58 specific styles */
458+
.demo-section {{
459+
background: #fff3cd;
460+
border: 1px solid #ffeaa7;
461+
border-radius: 6px;
462+
padding: 15px;
463+
margin: 15px 0;
464+
}}
465+
466+
.demo-section .btn-warning {{
467+
background-color: #ffc107;
468+
border-color: #ffc107;
469+
color: #212529;
470+
text-decoration: none;
471+
display: inline-block;
472+
padding: 8px 16px;
473+
border-radius: 4px;
474+
border: 1px solid transparent;
475+
font-weight: 400;
476+
text-align: center;
477+
vertical-align: middle;
478+
cursor: pointer;
479+
font-size: 1rem;
480+
line-height: 1.5;
481+
margin-top: 10px;
482+
}}
483+
484+
.demo-section .btn-warning:hover {{
485+
background-color: #e0a800;
486+
border-color: #d39e00;
487+
}}
488+
489+
/* Challenge explanation sections */
490+
.challenge-content {{
491+
margin-bottom: 30px;
492+
}}
493+
.explanation-content, .hint-content, .reason-content {{
494+
background: #f8f9fa;
495+
border: 1px solid #e9ecef;
496+
border-radius: 6px;
497+
padding: 15px;
498+
margin-bottom: 20px;
499+
}}
500+
.explanation-content h3, .hint-content h3, .reason-content h3 {{
501+
color: #495057;
502+
margin-top: 0;
503+
}}
504+
.explanation-content ul, .hint-content ul, .reason-content ul {{
505+
margin-bottom: 10px;
506+
}}
507+
.explanation-content li, .hint-content li, .reason-content li {{
508+
margin-bottom: 5px;
509+
}}
510+
</style>"""
511+
content = content.replace("<head>", f"<head>{head_additions}")
512+
513+
# Add preview banner for Challenge 58
514+
banner = f"""
515+
<div class="preview-banner">
516+
<div class="alert-heading">🗄️ Challenge 58 - Database Connection String Exposure (PR #{self.pr_number})</div>
517+
<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>
518+
</div>"""
519+
520+
if '<div class="container"' in content:
521+
content = content.replace(
522+
'<div class="container"', f'<div class="container">{banner}'
523+
)
524+
elif "<body>" in content:
525+
content = content.replace(
526+
"<body>", f'<body><div class="container">{banner}</div>'
527+
)
528+
529+
return content
530+
434531
def add_static_assets(self, content, template_name):
435532
"""Add embedded CSS and JS for the static preview."""
436533
if "<head>" in content:
@@ -632,6 +729,120 @@ def generate_stats_page(self):
632729

633730
return content
634731

732+
def generate_challenge58_page(self):
733+
"""Generate Challenge 58 (Database Connection String Exposure) page with embedded content."""
734+
template_path = self.templates_dir / "challenge.html"
735+
736+
if not template_path.exists():
737+
print(f"Warning: Template {template_path} not found")
738+
return self.generate_fallback_challenge58()
739+
740+
with open(template_path, "r", encoding="utf-8") as f:
741+
content = f.read()
742+
743+
# Mock Challenge 58 data
744+
mock_challenge = {
745+
"name": "Challenge 58: Database Connection String Exposure",
746+
"stars": "⭐⭐⭐",
747+
"tech": "LOGGING",
748+
"explanation": "challenge58.adoc",
749+
"hint": "challenge58_hint.adoc",
750+
"reason": "challenge58_reason.adoc",
751+
"link": "/challenge/challenge-58",
752+
}
753+
754+
# Replace challenge-specific Thymeleaf content
755+
content = re.sub(
756+
r'<span th:text="\$\{challenge\.name\}"[^>]*>[^<]*</span>',
757+
f'<span data-cy="challenge-title">{mock_challenge["name"]}</span>',
758+
content,
759+
)
760+
content = re.sub(
761+
r'<span[^>]*th:text="\$\{challenge\.stars\}"[^>]*>[^<]*</span>',
762+
f'<span>{mock_challenge["stars"]}</span>',
763+
content,
764+
)
765+
content = re.sub(
766+
r'<strong th:text="\$\{challenge\.tech\}"[^>]*>[^<]*</strong>',
767+
f'<strong>{mock_challenge["tech"]}</strong>',
768+
content,
769+
)
770+
content = re.sub(
771+
r'<span th:text="\'Welcome to challenge \'\s*\+\s*\$\{challenge\.name\}\s*\+\s*\'\.\'"></span>',
772+
f'<span>Welcome to challenge {mock_challenge["name"]}.</span>',
773+
content,
774+
)
775+
776+
# Replace the explanation section with Challenge 58 content
777+
explanation_pattern = (
778+
r'<div th:replace="~\{doc:__\$\{challenge\.explanation\}__\}"></div>'
779+
)
780+
781+
# Load actual Challenge 58 content from AsciiDoc files
782+
explanation_content = self.load_adoc_content("challenge58.adoc")
783+
hint_content = self.load_adoc_content("challenge58_hint.adoc")
784+
reason_content = self.load_adoc_content("challenge58_reason.adoc")
785+
786+
challenge58_explanation = f"""
787+
<div class="challenge-explanation">
788+
<div class="challenge-content">
789+
<h4>📖 Challenge Explanation</h4>
790+
<div class="explanation-content">
791+
{explanation_content}
792+
</div>
793+
794+
<h4>💡 Hints</h4>
795+
<div class="hint-content">
796+
{hint_content}
797+
</div>
798+
799+
<h4>🧠 Reasoning</h4>
800+
<div class="reason-content">
801+
{reason_content}
802+
</div>
803+
</div>
804+
805+
<div class="challenge-demo">
806+
<h4>🔗 Database Connection Error Demo</h4>
807+
<div class="demo-section">
808+
<p><strong>Try the vulnerable endpoint:</strong></p>
809+
<a href="/error-demo/database-connection" class="btn btn-warning">
810+
🚨 Trigger Database Connection Error
811+
</a>
812+
<p><small class="text-muted">This endpoint simulates a database connection failure that exposes the connection string with embedded credentials.</small></p>
813+
</div>
814+
</div>
815+
</div>
816+
"""
817+
content = re.sub(
818+
explanation_pattern, lambda m: challenge58_explanation, content
819+
)
820+
821+
# Process the template
822+
content = self.process_thymeleaf_syntax(content, "challenge58")
823+
824+
# Ensure we have a proper HTML structure with head
825+
if "<head>" not in content:
826+
# Add basic HTML structure
827+
content = f"""<!DOCTYPE html>
828+
<html lang="en">
829+
<head>
830+
<meta charset="UTF-8">
831+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
832+
<title>OWASP WrongSecrets - Challenge 58</title>
833+
</head>
834+
{content}
835+
</html>"""
836+
837+
# Add embedded CSS and styling for Challenge 58
838+
content = self.add_static_assets_challenge58(content)
839+
840+
# Add navigation
841+
nav = self.generate_navigation_html()
842+
content = content.replace("<body>", f"<body>{nav}")
843+
844+
return content
845+
635846
def generate_challenge57_page(self):
636847
"""Generate Challenge 57 (LLM Challenge) page with embedded content."""
637848
template_path = self.templates_dir / "challenge.html"
@@ -899,6 +1110,57 @@ def generate_fallback_challenge57_snippet(self):
8991110
</script>
9001111
"""
9011112

1113+
def generate_fallback_challenge58(self):
1114+
"""Generate a fallback Challenge 58 page if template is missing."""
1115+
return f"""<!DOCTYPE html>
1116+
<html lang="en">
1117+
<head>
1118+
<meta charset="UTF-8">
1119+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
1120+
<title>OWASP WrongSecrets - Challenge 58</title>
1121+
<style>
1122+
{self.embedded_css}
1123+
</style>
1124+
</head>
1125+
<body>
1126+
{self.generate_navigation_html()}
1127+
<div class="container mt-4">
1128+
<div class="preview-banner">
1129+
<div class="alert-heading">🗄️ Challenge 58 - Database Connection String Exposure (PR #{self.pr_number})</div>
1130+
<small>This is a live preview of Challenge 58 demonstrating how database credentials leak through error messages.</small>
1131+
</div>
1132+
1133+
<h1>Challenge 58: Database Connection String Exposure ⭐⭐⭐</h1>
1134+
<p>Welcome to Challenge 58: Database Connection String Exposure.</p>
1135+
1136+
<div class="alert alert-primary" role="alert">
1137+
<h6 class="alert-heading">🔍 Your Task</h6>
1138+
<p class="mb-2">Find the database password that gets exposed when the application fails to connect to the database.</p>
1139+
<p class="mb-0">💡 <strong>Visit:</strong> The <code>/error-demo/database-connection</code> endpoint to trigger the error.</p>
1140+
</div>
1141+
1142+
<div class="demo-section">
1143+
<h4>🔗 Database Connection Error Demo</h4>
1144+
<p><strong>Try the vulnerable endpoint:</strong></p>
1145+
<a href="/error-demo/database-connection" class="btn btn-warning">
1146+
🚨 Trigger Database Connection Error
1147+
</a>
1148+
<p><small class="text-muted">This endpoint simulates a database connection failure that exposes the connection string with embedded credentials.</small></p>
1149+
</div>
1150+
1151+
<form>
1152+
<div class="mb-3">
1153+
<label for="answerfield" class="form-label"><strong>🔑 Enter the database password you found:</strong></label>
1154+
<input type="text" class="form-control" id="answerfield" placeholder="Type the password here..."/>
1155+
<small class="form-text text-muted">💡 Tip: Look for the password in the database connection error message.</small>
1156+
</div>
1157+
<button class="btn btn-primary" type="button">🚀 Submit Answer</button>
1158+
<button class="btn btn-secondary" type="button" onclick="document.getElementById('answerfield').value='';">🗑️ Clear</button>
1159+
</form>
1160+
</div>
1161+
</body>
1162+
</html>"""
1163+
9021164
def generate_fallback_challenge57(self):
9031165
"""Generate a fallback Challenge 57 page if template is missing."""
9041166
return f"""<!DOCTYPE html>
@@ -1069,7 +1331,7 @@ def generate_fallback_challenge(self):
10691331
</html>"""
10701332

10711333
def generate_all_pages(self):
1072-
"""Generate all static pages with Challenge 57 as the featured challenge."""
1334+
"""Generate all static pages with Challenge 58 as the featured latest challenge."""
10731335
# Create pages directory
10741336
pages_dir = self.static_dir / f"pr-{self.pr_number}" / "pages"
10751337
pages_dir.mkdir(parents=True, exist_ok=True)
@@ -1078,8 +1340,9 @@ def generate_all_pages(self):
10781340
"welcome.html": self.generate_welcome_page(),
10791341
"about.html": self.generate_about_page(),
10801342
"stats.html": self.generate_stats_page(),
1081-
"challenge-57.html": self.generate_challenge57_page(), # Always render Challenge 57
1082-
"challenge-example.html": self.generate_challenge57_page(), # Use Challenge 57 as the example too
1343+
"challenge-57.html": self.generate_challenge57_page(), # LLM Challenge (AI category)
1344+
"challenge-58.html": self.generate_challenge58_page(), # Database Challenge (Latest)
1345+
"challenge-example.html": self.generate_challenge58_page(), # Use Challenge 58 as the latest example
10831346
}
10841347

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

10911354
print(f"Generated {len(pages)} static pages in {pages_dir}")
1092-
print(f"✅ Challenge 57 (LLM Security) is featured as the latest challenge")
1355+
print(
1356+
f"✅ Challenge 57 (LLM Security) and Challenge 58 (Database Connection String Exposure) are both available"
1357+
)
1358+
print(f"✅ Challenge 58 is featured as the latest challenge")
10931359
return pages_dir
10941360

10951361

.github/scripts/remove_pr_from_index.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def main():
1414

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

README.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
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.
1818

19-
Can you solve all the 57 challenges?
19+
Can you solve all the 58 challenges?
2020

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

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

128128
| **I want to...** | **Recommended Setup** | **Challenges Available** |
129129
|------------------|----------------------|--------------------------|
130-
| 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) |
130+
| 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) |
131131
| Run locally with Docker | [Basic Docker](#basic-docker-exercises) | Same as above, but on your machine |
132-
| Learn Kubernetes secrets | [K8s/Minikube Setup](#basic-k8s-exercise) | Kubernetes challenges (1-6, 8, 12-43, 48-57) |
133-
| Practice with cloud secrets | [Cloud Challenges](#cloud-challenges) | All challenges (1-57) |
132+
| Learn Kubernetes secrets | [K8s/Minikube Setup](#basic-k8s-exercise) | Kubernetes challenges (1-6, 8, 12-43, 48-58) |
133+
| Practice with cloud secrets | [Cloud Challenges](#cloud-challenges) | All challenges (1-87) |
134134
| Run a workshop/CTF | [CTF Setup](#ctf) | Customizable challenge sets |
135135
| Contribute to the project | [Development Setup](#notes-on-development) | All challenges + development tools |
136136

137137
## Basic docker exercises
138138

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

141141
For the basic docker exercises you currently require:
142142

@@ -208,7 +208,7 @@ Now you can try to find the secrets by means of solving the challenge offered at
208208
- [localhost:8080/challenge/challenge-55](http://localhost:8080/challenge/challenge-55)
209209
- [localhost:8080/challenge/challenge-56](http://localhost:8080/challenge/challenge-56)
210210
- [localhost:8080/challenge/challenge-57](http://localhost:8080/challenge/challenge-57)
211-
211+
- [localhost:8080/challenge/challenge-58](http://localhost:8080/challenge/challenge-58)
212212
</details>
213213

214214
Note that these challenges are still very basic, and so are their explanations. Feel free to file a PR to make them look
@@ -237,7 +237,7 @@ If you want to host WrongSecrets on Railway, you can do so by deploying [this on
237237

238238
## Basic K8s exercise
239239

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

242242
### Minikube based
243243

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

315315
## Vault exercises with minikube
316316

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

320320
- minikube with docker (or comment out line 8 and work at your own k8s setup),
@@ -332,7 +332,7 @@ This is because if you run the start script again it will replace the secret in
332332

333333
## Cloud Challenges
334334

335-
_Can be used for challenges 1-57_
335+
_Can be used for challenges 1-58_
336336

337337
**READ THIS**: Given that the exercises below contain IAM privilege escalation exercises,
338338
never run this on an account which is related to your production environment or can influence your account-over-arching

config/zap/rule-config.tsv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@
1818
10003 IGNORE Vulnerable JS Library
1919
90004 IGNORE Insufficient Site Isolation Against Spectre Vulnerability
2020
2 IGNORE Private IP Disclosure
21+
90022 IGNORE Application Error Disclosure

0 commit comments

Comments
 (0)