Client-seitige Durchsetzung von Server-seitiger Sicherheit

Beschreibung

Client-seitige Durchsetzung von Server-seitiger Sicherheit tritt auf, wenn eine Anwendung sich auf Client-seitigen Code (JavaScript, Mobile-App, Desktop-Anwendung) verlässt, um Sicherheitskontrollen durchzusetzen, die auf dem Server implementiert werden sollten. Angreifer können Client-seitige Kontrollen umgehen, indem sie JavaScript modifizieren, Proxy-Tools verwenden oder Backend-APIs direkt aufrufen. Jede Sicherheitsmaßnahme, die nur auf dem Client existiert, bietet keinen echten Schutz.

Risiko

Angreifer können Authentifizierung umgehen, indem sie Client-seitige Auth-Prüfungen modifizieren. Autorisierungskontrollen im Client-Code werden trivial umgangen. Preisberechnungen auf dem Client ermöglichen Manipulation von Finanztransaktionen. Geschäftslogik-Validierung in JavaScript kann deaktiviert werden. Client-seitige Datenfilterung verhindert keinen Zugriff auf sensible Informationen. Im Wesentlichen sollte jede Client-seitige Sicherheitskontrolle nur als empfehlend betrachtet werden - echte Durchsetzung muss serverseitig erfolgen.

Lösung

Implementieren Sie alle Sicherheitskontrollen auf dem Server. Verwenden Sie Client-seitige Validierung nur für Benutzererfahrung (sofortiges Feedback), niemals für Sicherheit. Gehen Sie davon aus, dass alle Client-Eingaben potenziell bösartig sind. Der Server muss alle Eingaben validieren, Autorisierung für jede Anfrage prüfen und Geschäftsregeln durchsetzen. Vertrauen Sie niemals Daten, die von Clients empfangen werden, selbst wenn Client-seitiger Code sie hätte validieren sollen.

Häufige Auswirkungen

AuswirkungDetails
AutorisierungBereich: Zugriffskontroll-Bypass

Client-seitige Auth-Prüfungen können deaktiviert werden.
IntegritätBereich: Datenmanipulation

Client-seitige Validierung kann umgangen werden, um ungültige Daten zu übermitteln.
FinanziellBereich: Betrug

Client-seitige Preis-/Mengenprüfungen ermöglichen finanzielle Manipulation.

Beispielcode + Lösungscode

Verwundbarer Code

// VERWUNDBAR: Client-seitige Authentifizierung
function checkLogin() {
    const isLoggedIn = localStorage.getItem('isLoggedIn');

    if (isLoggedIn === 'true') {
        showDashboard();  // Sicherheit: isLoggedIn in Konsole setzen
    } else {
        showLoginPage();
    }
}

// VERWUNDBAR: Client-seitige Autorisierung
function checkAdmin() {
    const userRole = localStorage.getItem('role');

    // Angreifer: localStorage.setItem('role', 'admin')
    if (userRole === 'admin') {
        showAdminPanel();  // Keine serverseitige Prüfung!
    }
}

// VERWUNDBAR: Client-seitige Preisberechnung
async function processOrder(items) {
    // Summe auf Client berechnen
    let total = 0;
    items.forEach(item => {
        total += item.price * item.quantity;
    });

    // Angreifer kann total vor dem Senden modifizieren
    const response = await fetch('/api/order', {
        method: 'POST',
        body: JSON.stringify({
            items: items,
            total: total  // Server vertraut diesem Wert!
        })
    });
}

// VERWUNDBAR: Client-seitige Datenfilterung
async function loadUserData() {
    const response = await fetch('/api/users');
    const users = await response.json();

    const currentUserId = getCurrentUserId();

    // Client filtert um nur eigene Daten zu zeigen
    // Aber API hat ALLE Benutzer zurückgegeben!
    const myData = users.filter(u => u.id === currentUserId);

    displayData(myData);
}

// VERWUNDBAR: Client-seitiges Feature-Toggle
const features = {
    canExport: false,
    canDelete: false,
    canViewReports: false
};

function initFeatures() {
    // Features vom Server holen aber komplett vertrauen
    // Angreifer: features.canDelete = true in Konsole
    if (features.canDelete) {
        showDeleteButton();
    }
}

// VERWUNDBAR: Versteckte Felder für Sicherheitsdaten
<form action="/transfer" method="POST">
    <input type="text" name="amount" />
    <input type="hidden" name="fromAccount" value="123456" />
    <!-- Angreifer kann verstecktes Feld modifizieren! -->
    <button type="submit">Überweisen</button>
</form>
<!-- VERWUNDBAR: UI-Elemente als Sicherheit verstecken -->
<div id="adminPanel" style="display: none;">
    <!-- Versteckt aber JavaScript funktioniert trotzdem! -->
    <button onclick="deleteAllUsers()">Alle löschen</button>
</div>

<script>
// Security through Obscurity - leicht umgangen
if (!isAdmin()) {
    document.getElementById('adminPanel').style.display = 'none';
}
// Angreifer: document.getElementById('adminPanel').style.display = 'block'
// Oder direkt aufrufen: deleteAllUsers()
</script>
// VERWUNDBAR: Client-Daten vertrauen
@WebServlet("/checkout")
public class VulnerableCheckoutServlet extends HttpServlet {

    protected void doPost(HttpServletRequest request,
                          HttpServletResponse response)
            throws ServletException, IOException {

        // Vertraut Client-gesendeter Summe!
        double total = Double.parseDouble(request.getParameter("total"));

        // Client könnte $1000-Bestellung auf $0.01 geändert haben
        chargeCustomer(total);

        // Items auch nicht validiert
        String[] items = request.getParameterValues("items");
        processOrder(items, total);
    }
}

// VERWUNDBAR: Client bestimmt Zugriffslevel
@WebServlet("/admin")
public class VulnerableAdminServlet extends HttpServlet {

    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response)
            throws ServletException, IOException {

        // Vertraut Client-Header!
        String role = request.getHeader("X-User-Role");

        // Angreifer fügt Header hinzu: X-User-Role: admin
        if ("admin".equals(role)) {
            showAdminData(response);
        }
    }
}
# VERWUNDBAR: Flask vertraut Client-Daten
from flask import Flask, request

app = Flask(__name__)

@app.route('/purchase', methods=['POST'])
def purchase_vulnerable():
    data = request.json

    # Vertraut Client-berechneten Werten!
    quantity = data['quantity']
    price = data['price']  # Client-gesendeter Preis!
    total = data['total']  # Client-gesendete Summe!

    # Angreifer kann price=0.01, total=0.01 setzen
    process_payment(total)
    ship_items(quantity)

    return {'status': 'success'}

@app.route('/user/data')
def get_user_data_vulnerable():
    # Gibt alle Daten zurück, verlässt sich auf Client zum Filtern
    all_data = get_all_user_data()

    # Client-JavaScript filtert nach Benutzer-ID
    # Aber Angreifer bekommt alle Daten von diesem Endpunkt!
    return jsonify(all_data)

Lösungscode

// SICHER: Client-seitig nur für UX, Server erzwingt
async function submitOrder(items) {
    // Client berechnet nur für Anzeige
    let displayTotal = items.reduce((sum, item) =>
        sum + item.price * item.quantity, 0
    );
    showTotal(displayTotal);

    // Nur Item-IDs und Mengen senden
    // Server berechnet tatsächliche Summe aus Datenbankpreisen
    const response = await fetch('/api/order', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${getToken()}`
        },
        body: JSON.stringify({
            items: items.map(i => ({
                productId: i.id,
                quantity: i.quantity
            }))
            // Kein Preis oder Summe gesendet!
        })
    });

    const result = await response.json();

    if (result.error) {
        showError(result.error);
    } else {
        showConfirmation(result.total);  // Server-berechnete Summe
    }
}

// SICHER: Features durch Server bei jeder Anfrage kontrolliert
async function loadDashboard() {
    // Server gibt nur Daten zurück, auf die Benutzer Zugriff hat
    const response = await fetch('/api/dashboard', {
        headers: {
            'Authorization': `Bearer ${getToken()}`
        }
    });

    if (response.status === 403) {
        showAccessDenied();
        return;
    }

    const data = await response.json();
    // Anzeigen was Server zurückgab - es hat bereits gefiltert
    displayDashboard(data);
}

// SICHER: API-Endpunkte erzwingen ihre eigene Autorisierung
async function deleteItem(itemId) {
    const response = await fetch(`/api/items/${itemId}`, {
        method: 'DELETE',
        headers: {
            'Authorization': `Bearer ${getToken()}`
        }
    });

    // Server prüft ob Benutzer dieses Item löschen kann
    if (response.status === 403) {
        showError('Sie können dieses Element nicht löschen');
    } else if (response.ok) {
        showSuccess('Element gelöscht');
    }
}
// SICHER: Server berechnet und validiert alles
@WebServlet("/checkout")
public class SafeCheckoutServlet extends HttpServlet {

    @Inject
    private ProductService productService;

    @Inject
    private AuthService authService;

    protected void doPost(HttpServletRequest request,
                          HttpServletResponse response)
            throws ServletException, IOException {

        // Authentifizierung verifizieren
        User user = authService.getAuthenticatedUser(request);
        if (user == null) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }

        // Bestellartikel parsen
        List<OrderItem> items = parseOrderItems(request);

        // SERVER berechnet Summe aus Datenbankpreisen
        BigDecimal total = BigDecimal.ZERO;
        for (OrderItem item : items) {
            // Preis aus Datenbank holen, nicht vom Client!
            Product product = productService.findById(item.getProductId());
            if (product == null) {
                sendError(response, "Ungültiges Produkt");
                return;
            }

            // Menge validieren
            if (item.getQuantity() < 1 || item.getQuantity() > 100) {
                sendError(response, "Ungültige Menge");
                return;
            }

            BigDecimal lineTotal = product.getPrice()
                .multiply(BigDecimal.valueOf(item.getQuantity()));
            total = total.add(lineTotal);
        }

        // Mit Server-berechneter Summe verarbeiten
        processOrder(user, items, total);
    }
}

// SICHER: Server-seitige Autorisierung
@WebServlet("/admin/*")
public class SafeAdminServlet extends HttpServlet {

    @Inject
    private AuthService authService;

    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response)
            throws ServletException, IOException {

        // Benutzer aus sicherer Session holen, nicht aus Headern!
        User user = authService.getAuthenticatedUser(request);

        if (user == null) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }

        // Rolle aus Datenbank prüfen, nicht vom Client!
        if (!user.hasRole(Role.ADMIN)) {
            response.sendError(HttpServletResponse.SC_FORBIDDEN);
            return;
        }

        // Benutzer ist authentifiziert und autorisiert
        serveAdminContent(request, response);
    }
}
# SICHER: Flask mit server-seitiger Sicherheit
from flask import Flask, request, jsonify, g
from functools import wraps

app = Flask(__name__)

def require_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        token = request.headers.get('Authorization', '').replace('Bearer ', '')
        user = verify_token(token)
        if not user:
            return jsonify({'error': 'Nicht autorisiert'}), 401
        g.user = user
        return f(*args, **kwargs)
    return decorated

def require_admin(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        if not g.user.is_admin:
            return jsonify({'error': 'Verboten'}), 403
        return f(*args, **kwargs)
    return decorated

@app.route('/purchase', methods=['POST'])
@require_auth
def purchase_safe():
    data = request.json

    # Nur Produkt-IDs und Mengen akzeptieren
    items = data.get('items', [])

    # Summe aus DATENBANK-Preisen berechnen
    total = Decimal('0')
    order_items = []

    for item in items:
        product = Product.query.get(item['product_id'])
        if not product:
            return jsonify({'error': 'Ungültiges Produkt'}), 400

        quantity = item['quantity']
        if not (1 <= quantity <= 100):
            return jsonify({'error': 'Ungültige Menge'}), 400

        line_total = product.price * quantity
        total += line_total
        order_items.append({
            'product': product,
            'quantity': quantity,
            'price': product.price
        })

    # Mit Server-berechneter Summe verarbeiten
    order = process_order(g.user, order_items, total)
    return jsonify({'order_id': order.id, 'total': str(total)})

@app.route('/user/data')
@require_auth
def get_user_data_safe():
    # NUR Daten dieses Benutzers zurückgeben
    user_data = UserData.query.filter_by(user_id=g.user.id).all()
    return jsonify([d.to_dict() for d in user_data])

@app.route('/admin/users')
@require_auth
@require_admin
def admin_users():
    # Server erzwingt Admin-Prüfung
    users = User.query.all()
    return jsonify([u.to_dict() for u in users])

Ausgenutzt in der Praxis

Preismanipulation

E-Commerce-Seiten, die Client-gesendeten Preisen vertrauten, wurden für kostenlose/vergünstigte Produkte ausgenutzt.

Autorisierungs-Bypass

Anwendungen, die Admin-Features mit JavaScript versteckten, hatten diese Features direkt ausgenutzt.

Datendiebstahl

APIs, die alle Daten mit Client-seitiger Filterung zurückgaben, exponierten sensible Informationen.


Tools zum Testen/Ausnutzen

  • Burp Suite — Anfragen abfangen und modifizieren
  • Browser-Entwicklertools — JavaScript, localStorage modifizieren
  • Postman — APIs direkt aufrufen
  • OWASP ZAP — automatisierte Sicherheitstests

CVE-Beispiele

  • CVEs durch Preismanipulation in E-Commerce
  • Zugriffskontroll-Bypass durch Client-Modifikation
  • Daten-Exposure durch Client-gefilterte APIs

Referenzen

  1. MITRE. "CWE-602: Client-Side Enforcement of Server-Side Security." https://cwe.mitre.org/data/definitions/602.html
  2. OWASP. "Testing for Client-side Testing." https://owasp.org/