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
+1175 -6
View File
File diff suppressed because it is too large Load Diff
+3 -2
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",
+223
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;
@@ -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;
@@ -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) => (
@@ -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;
@@ -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;
@@ -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>
)}
@@ -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;
@@ -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;
+79 -12
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>
+54 -11
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',
+17 -1
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'),
+122
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;
}