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

AuswirkungDetails
AutorisierungBereich: Privilegieneskalation

Benutzer erhalten unbefugte Zugriffsstufen.
IntegritätBereich: Zustandsmanipulation

Anwendungszustand durch Angreifer korrumpiert.
AuthentifizierungBereich: 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

  1. MITRE. "CWE-642: External Control of Critical State Data." https://cwe.mitre.org/data/definitions/642.html
  2. OWASP. "Insecure Client-Side Storage."