medienkompetenz-lernplattform/backend/lessons/modules/sql-injection-shop/index.js
Marius Rometsch a439873394 Add lessons
2026-02-08 19:47:21 +01:00

375 lines
14 KiB
JavaScript

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 {
products: [
{ id: 1, name: 'Laptop Pro 15', price: 1299.99, category: 'Electronics', stock: 15 },
{ 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 }
],
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' }
]
};
}
// Simulate vulnerable SQL query
async executeVulnerableQuery(searchTerm, participantId, eventLessonId) {
const db = this.getMockDatabase();
// Build the "vulnerable" query string for educational display
const vulnerableQuery = `SELECT * FROM products WHERE name LIKE '%${searchTerm}%'`;
// Detect SQL injection attempts
const injectionDetected = this.detectInjection(searchTerm);
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 === 'BYPASS_FILTER') {
// Return all products
results = db.products;
} else if (injectionInfo.type === 'UNION_SELECT') {
// Simulate UNION attack showing user data
results = [
{ 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 === 'GENERIC') {
// Generic injection - show it affects the query
results = db.products;
}
} else {
// Normal search - filter products by name
results = db.products.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}
const totalDiscoveries = 3; // Only 3 challenges now
const elapsedTime = await this._getElapsedTime(participantId, eventLessonId);
return {
query: vulnerableQuery,
results,
injectionDetected,
injectionType,
explanation,
recordCount: results.length,
pointsAwarded: isNewDiscovery ? pointsAwarded : 0,
isNewDiscovery,
unionHintMessage,
discoveries: {
found: discoveries.size,
total: totalDiscoveries,
types: Array.from(discoveries)
},
elapsedTime
};
}
// Detect if input contains SQL injection
detectInjection(input) {
const injectionPatterns = [
/'/, // Single quote
/union/i, // UNION keyword
/select/i, // SELECT keyword
/or\s+['"]?\d+['"]?\s*=\s*['"]?\d+['"]?/i // OR 1=1 pattern
];
return injectionPatterns.some(pattern => pattern.test(input));
}
// Analyze the type of SQL injection
analyzeInjection(input) {
const lowerInput = input.toLowerCase();
// 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: '🎉 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: 'BYPASS_FILTER',
explanation: "✅ Super! Die Bedingung '1'='1' ist immer wahr und umgeht den Filter. Jetzt werden ALLE Produkte angezeigt. **Challenge 2 abgeschlossen!**"
};
}
}
// Challenge 1: Generic injection - just discovered manipulation is possible
if (lowerInput.includes("'")) {
return {
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!**"
};
}
return {
type: 'NONE',
explanation: 'Keine SQL Injection erkannt.'
};
}
// Demonstrate safe parameterized query
executeSafeQuery(searchTerm) {
const db = this.getMockDatabase();
const safeQuery = `SELECT * FROM products WHERE name LIKE ?`;
const parameter = `%${searchTerm}%`;
const results = db.products.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase())
);
return {
query: safeQuery,
parameter,
results,
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
async getInteractiveData(stepId) {
if (stepId === 'shop-demo') {
return {
timerDuration: 600, // 10 minutes
totalChallenges: 3, // Simplified to 3 challenges
database: this.getMockDatabase(),
challenges: [
{
id: 'GENERIC',
difficulty: 'Anfänger',
points: 30,
title: 'SQL Injection entdecken',
hint: "Versuche ein ' (Anführungszeichen) einzugeben"
},
{
id: 'BYPASS_FILTER',
difficulty: 'Anfänger',
points: 40,
title: 'Filter umgehen (alle Produkte zeigen)',
hint: "Verwende: ' OR '1'='1"
},
{
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 await super.getInteractiveData(stepId);
}
}
module.exports = SQLInjectionShopLesson;