Externe Kontrolle über kritische Zustandsdaten
Beschreibung
Externe Kontrolle über kritische Zustandsdaten tritt auf, wenn sicherheitskritischer Anwendungszustand (Benutzerrollen, Berechtigungen, Session-Daten, Authentifizierungsstatus) an Orten gespeichert wird, die von Benutzern kontrolliert oder modifizierbar sind. Dies umfasst Cookies, versteckte Formularfelder, clientseitigen Speicher, URL-Parameter oder unverschlüsselte clientseitig zugängliche Daten. Angreifer können diesen Zustand modifizieren, um Privilegien zu eskalieren oder Sicherheitskontrollen zu umgehen.
Risiko
Privilegieneskalation durch Modifizieren von Rollen-/Berechtigungsdaten. Authentifizierungs-Bypass durch Manipulation des Session-Zustands. Zugriff auf eingeschränkte Funktionen durch Ändern von Feature-Flags. Preismanipulation im E-Commerce durch Modifizieren clientseitig gespeicherter Preise. Umgehen von Geschäftslogik durch Ändern des Workflow-Zustands. Vollständige Kontoübernahme durch Zustandsmanipulation.
Lösung
Speichern Sie kritischen Zustand nur serverseitig. Vertrauen Sie niemals clientseitig bereitgestellten Zustandsdaten. Verwenden Sie kryptographisch signierte Tokens für alle clientseitig gespeicherten Daten. Verifizieren Sie Zustandsintegrität bei jeder Serveranfrage. Implementieren Sie serverseitiges Session-Management. Revalidieren Sie Berechtigungen von autoritativer Quelle bei jeder Anfrage.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Autorisierung | Bereich: Privilegieneskalation Benutzer erhalten unbefugte Zugriffsstufen. |
| Integrität | Bereich: Zustandsmanipulation Anwendungszustand durch Angreifer korrumpiert. |
| Authentifizierung | Bereich: Bypass Auth-Status manipuliert, um Zugang zu erhalten. |
Beispielcode + Lösungscode
Verwundbarer Code
// VERWUNDBAR: Rolle in Cookie gespeichert
@WebServlet("/dashboard")
public class VulnerableDashboard extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
// Rolle aus Cookie lesen - benutzersteuerbar!
Cookie[] cookies = req.getCookies();
String role = "user"; // Standard
for (Cookie c : cookies) {
if ("userRole".equals(c.getName())) {
role = c.getValue(); // Angreifer setzt auf "admin"
}
}
if ("admin".equals(role)) {
showAdminPanel(resp); // Privilegieneskalation!
}
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
// Rolle aus verstecktem Formularfeld - Benutzer kann modifizieren
String role = req.getParameter("role"); // Verstecktes Feld
if ("admin".equals(role)) {
performAdminAction(req);
}
}
}
// VERWUNDBAR: Authentifizierungsstatus im Client
public class VulnerableAuth {
public void login(HttpServletResponse resp, String username) {
// Auth-Status in Plain-Cookie gespeichert
Cookie authCookie = new Cookie("authenticated", "true");
Cookie userCookie = new Cookie("username", username);
Cookie adminCookie = new Cookie("isAdmin", "false");
resp.addCookie(authCookie);
resp.addCookie(userCookie);
resp.addCookie(adminCookie);
// Angreifer modifiziert isAdmin=true
}
}
# VERWUNDBAR: Flask mit clientseitigem Zustand
from flask import Flask, request, make_response
app = Flask(__name__)
@app.route('/checkout', methods=['POST'])
def checkout_vulnerable():
# Preis aus verstecktem Formularfeld - Benutzer kann modifizieren!
price = float(request.form['total_price']) # War 99,99€
# Angreifer ändert auf 0,01€
process_payment(price)
return "Bestellung aufgegeben!"
@app.route('/profile')
def profile_vulnerable():
# Benutzer-ID aus Cookie - jeder kann ändern
user_id = request.cookies.get('user_id') # Angreifer setzt auf Admin-ID
user_data = db.get_user(user_id)
return render_template('profile.html', user=user_data)
@app.route('/admin')
def admin_vulnerable():
# Admin-Prüfung aus Client-Cookie
is_admin = request.cookies.get('is_admin') == 'true'
if is_admin: # Angreifer setzt Cookie
return render_admin_panel()
return "Zugriff verweigert", 403
# VERWUNDBAR: Workflow-Zustand in URL
@app.route('/order/<order_id>/status/<status>')
def update_order_vulnerable(order_id, status):
# Status aus URL - Benutzer kann direkt navigieren
# /order/123/status/shipped - Zahlung überspringen!
db.update_order_status(order_id, status)
return f"Bestellung {order_id} ist jetzt {status}"
// VERWUNDBAR: Clientseitiges Rollenmanagement
class VulnerableApp {
constructor() {
// Rolle in localStorage gespeichert - Benutzer kann über DevTools modifizieren
this.role = localStorage.getItem('userRole') || 'guest';
this.isAdmin = localStorage.getItem('isAdmin') === 'true';
}
checkPermission(action) {
// Clientseitige Berechtigungsprüfung
if (this.role === 'admin' || this.isAdmin) {
return true; // Angreifer setzt localStorage.isAdmin = 'true'
}
return false;
}
async deleteUser(userId) {
if (this.checkPermission('delete_user')) {
// Server vertraut clientseitiger Berechtigungsprüfung!
await fetch(`/api/users/${userId}`, { method: 'DELETE' });
}
}
}
// VERWUNDBAR: Versteckte Formularfelder
/*
<form action="/transfer" method="POST">
<input type="hidden" name="fromAccount" value="12345">
<input type="hidden" name="maxLimit" value="1000">
<!-- Angreifer modifiziert maxLimit auf 999999 -->
<input type="text" name="amount">
<button type="submit">Überweisen</button>
</form>
*/
app.post('/transfer', (req, res) => {
const { fromAccount, maxLimit, amount } = req.body;
// Vertraut maxLimit aus Formular!
if (amount <= maxLimit) {
transferFunds(fromAccount, amount);
}
});
// VERWUNDBAR: JWT ohne ordnungsgemäße Validierung
app.use((req, res, next) => {
const token = req.cookies.token;
if (token) {
// Nur dekodieren, nicht Signatur verifizieren!
const decoded = jwt.decode(token); // NICHT verify()
req.user = decoded; // Angreifer fälscht Token
}
next();
});
<?php
// VERWUNDBAR: Session-Daten aus Cookie
$user_role = $_COOKIE['user_role']; // Benutzergesteuert
if ($user_role === 'administrator') {
include 'admin_panel.php'; // Privilegieneskalation
}
// VERWUNDBAR: Preismanipulation
$price = $_POST['item_price']; // Aus verstecktem Feld
$quantity = $_POST['quantity'];
$total = $price * $quantity; // Angreifer setzte Preis auf 0,01
processPayment($total);
// VERWUNDBAR: Benutzer-ID aus GET-Parameter
$viewing_user = $_GET['user_id']; // Jeder kann jeden Benutzer sehen
$user_data = fetchUserData($viewing_user);
display($user_data);
// VERWUNDBAR: Rabattcode-Validierung auf Client
$discount = $_POST['discount_percentage']; // Verstecktes Feld
// Angreifer ändert von 10 auf 100
$final_price = $original_price * (1 - $discount / 100);
// VERWUNDBAR: Kontonummer aus Formular
$from_account = $_POST['from_account']; // Verstecktes Feld
$to_account = $_POST['to_account'];
$amount = $_POST['amount'];
// Angreifer ändert from_account auf fremdes Konto
transferMoney($from_account, $to_account, $amount);
?>
Lösungscode
// SICHER: Serverseitiges Session-Zustandsmanagement
@WebServlet("/dashboard")
public class SafeDashboard extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
// Rolle nur aus serverseitiger Session holen
HttpSession session = req.getSession(false);
if (session == null) {
resp.sendRedirect("/login");
return;
}
// Rolle aus Session, niemals vom Client
User user = (User) session.getAttribute("user");
if (user == null) {
resp.sendRedirect("/login");
return;
}
// Gegen Datenbank für kritische Operationen verifizieren
User freshUser = userService.findById(user.getId());
if (freshUser.hasRole("admin")) {
showAdminPanel(resp);
}
}
}
// SICHER: Signierte Tokens für Client-Zustand
public class SafeAuth {
private final String secretKey = System.getenv("JWT_SECRET");
public String createAuthToken(User user) {
return Jwts.builder()
.setSubject(user.getId().toString())
.claim("roles", user.getRoles())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 3600000))
.signWith(Keys.hmacShaKeyFor(secretKey.getBytes()))
.compact();
}
public User validateToken(String token) {
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey.getBytes())
.build()
.parseClaimsJws(token)
.getBody();
// Verifizieren, dass Benutzer noch existiert und beanspruchte Rollen hat
User user = userService.findById(Long.parseLong(claims.getSubject()));
if (user == null) {
throw new UnauthorizedException();
}
return user;
} catch (JwtException e) {
throw new UnauthorizedException("Ungültiges Token");
}
}
}
# SICHER: Serverseitiges Zustandsmanagement
from flask import Flask, session, request, redirect
from functools import wraps
import hmac
import hashlib
app = Flask(__name__)
app.secret_key = os.environ['SECRET_KEY']
def require_admin(f):
@wraps(f)
def decorated(*args, **kwargs):
# Serverseitige Session prüfen
user_id = session.get('user_id')
if not user_id:
return redirect('/login')
# Gegen Datenbank verifizieren
user = db.get_user(user_id)
if not user or 'admin' not in user.roles:
return "Zugriff verweigert", 403
return f(*args, **kwargs)
return decorated
@app.route('/checkout', methods=['POST'])
def checkout_safe():
# Preis vom Server holen, nicht vom Client
cart = get_user_cart(session['user_id'])
total_price = calculate_total(cart) # Server berechnet
process_payment(total_price)
return "Bestellung aufgegeben!"
@app.route('/profile')
def profile_safe():
# Authentifizierten Session-Benutzer verwenden
user_id = session.get('user_id')
if not user_id:
return redirect('/login')
user_data = db.get_user(user_id)
return render_template('profile.html', user=user_data)
@app.route('/admin')
@require_admin
def admin_safe():
return render_admin_panel()
# SICHER: Servergesteuerter Workflow
@app.route('/order/<order_id>/ship', methods=['POST'])
@require_admin
def ship_order_safe(order_id):
order = db.get_order(order_id)
# Bestellungszustand auf Server verifizieren
if order.status != 'paid':
return "Unbezahlte Bestellung kann nicht versendet werden", 400
db.update_order_status(order_id, 'shipped')
return f"Bestellung {order_id} versendet"
// SICHER: Serverseitiger Zustand mit signierten Tokens
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET;
// Serverseitiger Session-Speicher
const sessionStore = new Map();
class SafeAuth {
createSession(user) {
const sessionId = crypto.randomUUID();
sessionStore.set(sessionId, {
userId: user.id,
roles: user.roles,
createdAt: Date.now()
});
// Signierte Session-ID zurückgeben
return jwt.sign({ sessionId }, SECRET, { expiresIn: '1h' });
}
validateSession(token) {
try {
const { sessionId } = jwt.verify(token, SECRET);
const session = sessionStore.get(sessionId);
if (!session) {
throw new Error('Session nicht gefunden');
}
return session;
} catch (error) {
throw new UnauthorizedError('Ungültige Session');
}
}
}
// SICHER: Serverseitige Berechtigungsprüfungen
app.post('/transfer', async (req, res) => {
const session = auth.validateSession(req.cookies.token);
// Konto aus Session holen, nicht aus Formular
const fromAccount = await db.getUserAccount(session.userId);
// Überweisungslimit aus Datenbank holen, nicht aus Formular
const user = await db.getUser(session.userId);
const maxLimit = user.transferLimit;
const { toAccount, amount } = req.body;
if (amount > maxLimit) {
return res.status(400).json({ error: 'Limit überschritten' });
}
await transferFunds(fromAccount.id, toAccount, amount);
res.json({ success: true });
});
// SICHER: Preis vom Server
app.post('/checkout', async (req, res) => {
const session = auth.validateSession(req.cookies.token);
const cart = await db.getUserCart(session.userId);
// Preis serverseitig berechnen
const total = cart.items.reduce((sum, item) => {
const product = db.getProduct(item.productId);
return sum + (product.price * item.quantity);
}, 0);
await processPayment(total);
res.json({ success: true, total });
});
<?php
// SICHER: Serverseitiges Session-Management
session_start();
// Rolle aus Session, nicht aus Cookie
function getUserRole() {
if (!isset($_SESSION['user_id'])) {
return null;
}
// Gegen Datenbank verifizieren
global $db;
$user = $db->query(
"SELECT role FROM users WHERE id = ?",
[$_SESSION['user_id']]
)->fetch();
return $user ? $user['role'] : null;
}
// SICHER: Admin-Prüfung
if (getUserRole() === 'administrator') {
include 'admin_panel.php';
}
// SICHER: Preis vom Server
function checkout() {
$cart = getCartFromSession();
$total = 0;
foreach ($cart as $item) {
// Preis aus Datenbank holen, nicht vom Client
$product = getProductFromDatabase($item['product_id']);
$total += $product['price'] * $item['quantity'];
}
processPayment($total);
}
// SICHER: Benutzeroperationen nur auf authentifizierten Benutzer
function viewProfile() {
if (!isset($_SESSION['user_id'])) {
header('Location: /login');
exit;
}
$user_data = fetchUserData($_SESSION['user_id']);
display($user_data);
}
// SICHER: Überweisung mit serverseitiger Validierung
function transfer($to_account, $amount) {
if (!isset($_SESSION['user_id'])) {
throw new Exception('Nicht authentifiziert');
}
// VON-Konto aus Session-Benutzer holen, nicht aus Formular
$from_account = getUserPrimaryAccount($_SESSION['user_id']);
// Limit aus Datenbank holen
$limit = getUserTransferLimit($_SESSION['user_id']);
if ($amount > $limit) {
throw new Exception('Limit überschritten');
}
transferMoney($from_account, $to_account, $amount);
}
?>
Ausgenutzt in der Praxis
E-Commerce-Preismanipulation
Versteckte-Feld-Modifikationen für kostenlose Käufe.
Privilegieneskalation
Cookie-basierte Rollenmanipulation.
Kontoübernahme
Clientseitig gespeicherte Benutzer-IDs ermöglichen Impersonation.
Tools zum Testen/Ausnutzen
- Browser-Entwicklertools für Cookie-/Speicher-Manipulation.
- Burp Suite — Parameter-Tampering.
- OWASP ZAP Proxy.
CVE-Beispiele
- CVE-2017-12615: Apache Tomcat verwendete clientseitig bereitgestellten Zustand.
- Zahlreiche Privilegieneskalations-CVEs über Client-Zustand.
Referenzen
- MITRE. "CWE-642: External Control of Critical State Data." https://cwe.mitre.org/data/definitions/642.html
- OWASP. "Insecure Client-Side Storage."