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
| Auswirkung | Details |
|---|---|
| Autorisierung | Bereich: Zugriffskontroll-Bypass Client-seitige Auth-Prüfungen können deaktiviert werden. |
| Integrität | Bereich: Datenmanipulation Client-seitige Validierung kann umgangen werden, um ungültige Daten zu übermitteln. |
| Finanziell | Bereich: 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
- MITRE. "CWE-602: Client-Side Enforcement of Server-Side Security." https://cwe.mitre.org/data/definitions/602.html
- OWASP. "Testing for Client-side Testing." https://owasp.org/