Autorisierungs-Bypass durch benutzergesteuerten Schlüssel
Beschreibung
Autorisierungs-Bypass durch benutzergesteuerten Schlüssel tritt auf, wenn die Autorisierungsfunktionalität des Systems nicht verhindert, dass ein Benutzer auf Daten eines anderen Benutzers zugreift, indem er den Schlüsselwert ändert, der die Daten identifiziert. Dies ist allgemein als Insecure Direct Object Reference (IDOR) bekannt. Die Schwachstelle entsteht, wenn Anwendungen benutzergelieferte Eingaben (wie IDs, Dateinamen oder Schlüssel) verwenden, um direkt auf Objekte zuzugreifen, ohne die Autorisierung des Benutzers für das spezifische Objekt zu verifizieren. Angreifer modifizieren einfach diese Referenzen, um auf unbefugte Ressourcen zuzugreifen.
Risiko
IDOR-Schwachstellen sind extrem häufig und oft kritisch. Sie können sensible Daten anderer Benutzer exponieren, unbefugte Änderungen ermöglichen und zu vollständigen Datenverletzungen führen. Da die Ausnutzung trivial ist (einfach eine ID ändern), können automatisierte Tools schnell große Datenmengen enumerieren und extrahieren. Bemerkenswerte Datenverletzungen, die Millionen von Benutzern betrafen, resultierten aus IDOR-Schwachstellen. Die First American Financial-Exposition von 885 Millionen Datensätzen wurde durch sequentielle Dokument-IDs ohne Autorisierungsprüfungen verursacht.
Lösung
Verifizieren Sie immer, dass der authentifizierte Benutzer Berechtigung zum Zugriff auf die angeforderte Ressource hat. Implementieren Sie Autorisierungsprüfungen auf Objektebene. Verwenden Sie indirekte Referenzen, die serverseitig auf tatsächliche Objekte abbilden. Erwägen Sie die Verwendung von UUIDs anstelle von sequentiellen IDs, um Vorhersagbarkeit zu reduzieren (aber implementieren Sie dennoch Autorisierung). Implementieren Sie ordnungsgemäße Zugriffskontrolllisten. Protokollieren und überwachen Sie Zugriffsmuster auf Anomalien. Testen Sie Autorisierung auf jedem Zugriffspfad zu sensiblen Ressourcen.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Vertraulichkeit | Bereich: Datenexposition Unbefugter Zugriff auf persönliche Daten, Dokumente und Datensätze anderer Benutzer. |
| Integrität | Bereich: Datenänderung Fähigkeit, Daten anderer Benutzer durch Manipulation von Objektreferenzen zu modifizieren oder zu löschen. |
| Privatsphäre | Bereich: Privatsphärenverletzung Zugriff auf private Kommunikationen, medizinische Aufzeichnungen, Finanzdaten anderer Benutzer. |
Beispielcode + Lösungscode
Verwundbarer Code
# VERWUNDBAR: Direkte Objektreferenz ohne Autorisierung
@app.route('/api/users/<user_id>/profile')
@login_required
def get_profile(user_id):
# Jeder authentifizierte Benutzer kann auf jedes Profil zugreifen!
user = User.query.get(user_id)
return jsonify(user.to_dict())
# VERWUNDBAR: Dokumentzugriff per ID
@app.route('/api/documents/<doc_id>')
@login_required
def get_document(doc_id):
# Einfaches Inkrementieren von doc_id exponiert alle Dokumente
document = Document.query.get(doc_id)
return send_file(document.path)
# VERWUNDBAR: Bestelldetails
@app.route('/api/orders/<order_id>')
@login_required
def get_order(order_id):
# Angreifer ändert order_id um Bestellungen anderer Benutzer zu sehen
order = Order.query.get(order_id)
return jsonify({
'items': order.items,
'total': order.total,
'shipping_address': order.shipping_address, # PII exponiert!
'payment_method': order.payment_method
})
# VERWUNDBAR: Datei-Download
@app.route('/download')
@login_required
def download_file():
filename = request.args.get('file')
# Angreifer: ?file=../../../etc/passwd oder ?file=user_123_data.pdf
return send_from_directory('/uploads', filename)
// VERWUNDBAR: REST-Endpunkt mit IDOR
@RestController
@RequestMapping("/api")
public class UserController {
@GetMapping("/users/{userId}/messages")
public List<Message> getMessages(@PathVariable Long userId) {
// Keine Prüfung ob authentifizierter Benutzer userId ist!
return messageRepository.findByUserId(userId);
}
@DeleteMapping("/invoices/{invoiceId}")
public ResponseEntity<?> deleteInvoice(@PathVariable Long invoiceId) {
// Jeder Benutzer kann jede Rechnung löschen
invoiceRepository.deleteById(invoiceId);
return ResponseEntity.ok().build();
}
@PutMapping("/accounts/{accountId}")
public ResponseEntity<?> updateAccount(
@PathVariable Long accountId,
@RequestBody AccountUpdate update) {
// Angreifer modifiziert jedes Konto
Account account = accountRepository.findById(accountId).orElseThrow();
account.setEmail(update.getEmail());
accountRepository.save(account);
return ResponseEntity.ok().build();
}
}
// VERWUNDBAR: Express API mit IDOR
app.get('/api/users/:userId/data', authenticate, async (req, res) => {
// userId aus URL, nicht gegen authentifizierten Benutzer validiert
const data = await UserData.findOne({ userId: req.params.userId });
res.json(data); // Gibt Daten jedes Benutzers zurück
});
// VERWUNDBAR: GraphQL-Abfrage
const resolvers = {
Query: {
user: (_, { id }) => {
// Keine Autorisierungsprüfung
return User.findById(id);
},
invoice: (_, { id }) => {
// Jeder kann jede Rechnung abfragen
return Invoice.findById(id);
}
}
};
// VERWUNDBAR: Dateibereitstellung
app.get('/files/:fileId', authenticate, async (req, res) => {
const file = await File.findById(req.params.fileId);
// Keine Eigentümerschaftsprüfung
res.sendFile(file.path);
});
Lösungscode
# SICHER: Eigentümerschaft vor Zugriff verifizieren
@app.route('/api/users/<user_id>/profile')
@login_required
def get_profile(user_id):
# Verifizieren, dass Benutzer eigenes Profil abruft oder Admin ist
if str(current_user.id) != user_id and not current_user.is_admin:
abort(403, 'Zugriff verweigert')
user = User.query.get_or_404(user_id)
return jsonify(user.to_dict())
# SICHER: Dokumenteneigentümerschaft prüfen
@app.route('/api/documents/<doc_id>')
@login_required
def get_document(doc_id):
document = Document.query.get_or_404(doc_id)
# Eigentümerschaft oder Freigabeberechtigung verifizieren
if not document.can_access(current_user):
abort(403, 'Zugriff verweigert')
return send_file(document.path)
# SICHER: Bestellungen nach authentifiziertem Benutzer filtern
@app.route('/api/orders/<order_id>')
@login_required
def get_order(order_id):
# Abfrage enthält Benutzereinschränkung
order = Order.query.filter_by(
id=order_id,
user_id=current_user.id
).first_or_404()
return jsonify(order.to_dict())
# SICHER: Indirekte Referenzen verwenden
ALLOWED_DOWNLOADS = {
'report': 'monthly_report.pdf',
'invoice': 'invoice_template.pdf',
'guide': 'user_guide.pdf'
}
@app.route('/download/<file_key>')
@login_required
def download_file(file_key):
filename = ALLOWED_DOWNLOADS.get(file_key)
if not filename:
abort(404)
# Benutzerspezifische Dateien
user_path = f'/uploads/{current_user.id}'
return send_from_directory(user_path, filename)
# SICHER: Objektebenen-Autorisierungs-Decorator
def authorize_resource(model, id_param='id', owner_field='user_id'):
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
resource_id = kwargs.get(id_param)
resource = model.query.get_or_404(resource_id)
if getattr(resource, owner_field) != current_user.id:
if not current_user.is_admin:
abort(403)
kwargs['resource'] = resource
return f(*args, **kwargs)
return wrapper
return decorator
@app.route('/api/posts/<post_id>')
@login_required
@authorize_resource(Post, 'post_id', 'author_id')
def get_post(post_id, resource):
return jsonify(resource.to_dict())
// SICHER: Service-Layer-Autorisierung
@Service
public class SecureUserService {
@Autowired
private MessageRepository messageRepository;
public List<Message> getMessages(Long userId, Authentication auth) {
User currentUser = (User) auth.getPrincipal();
// Autorisierung verifizieren
if (!currentUser.getId().equals(userId) &&
!currentUser.hasRole("ADMIN")) {
throw new AccessDeniedException("Zugriff auf Nachrichten anderer Benutzer nicht möglich");
}
return messageRepository.findByUserId(userId);
}
}
@RestController
@RequestMapping("/api")
public class SecureController {
@Autowired
private SecureUserService userService;
@GetMapping("/users/{userId}/messages")
public List<Message> getMessages(
@PathVariable Long userId,
Authentication auth) {
return userService.getMessages(userId, auth);
}
@DeleteMapping("/invoices/{invoiceId}")
@PreAuthorize("@invoiceSecurityService.canDelete(#invoiceId, authentication)")
public ResponseEntity<?> deleteInvoice(@PathVariable Long invoiceId) {
invoiceRepository.deleteById(invoiceId);
return ResponseEntity.ok().build();
}
}
@Service
public class InvoiceSecurityService {
@Autowired
private InvoiceRepository invoiceRepository;
public boolean canDelete(Long invoiceId, Authentication auth) {
User user = (User) auth.getPrincipal();
Invoice invoice = invoiceRepository.findById(invoiceId).orElse(null);
if (invoice == null) return false;
// Eigentümer oder Admin kann löschen
return invoice.getUserId().equals(user.getId()) ||
user.hasRole("ADMIN");
}
}
// SICHER: Autorisierungs-Middleware
const authorizeResource = (model, ownerField = 'userId') => {
return async (req, res, next) => {
const resourceId = req.params.id || req.params[`${model.modelName.toLowerCase()}Id`];
const resource = await model.findById(resourceId);
if (!resource) {
return res.status(404).json({ error: 'Nicht gefunden' });
}
// Eigentümerschaft prüfen
if (resource[ownerField].toString() !== req.user.id &&
!req.user.roles.includes('admin')) {
return res.status(403).json({ error: 'Zugriff verweigert' });
}
req.resource = resource;
next();
};
};
// Verwendung
app.get('/api/users/:id/data',
authenticate,
authorizeResource(UserData),
async (req, res) => {
res.json(req.resource);
}
);
// SICHER: GraphQL mit Autorisierung
const resolvers = {
Query: {
user: async (_, { id }, context) => {
if (!context.user) {
throw new AuthenticationError('Nicht authentifiziert');
}
// Kann nur eigenen Benutzer abfragen oder wenn Admin
if (context.user.id !== id && !context.user.isAdmin) {
throw new ForbiddenError('Zugriff verweigert');
}
return User.findById(id);
},
invoice: async (_, { id }, context) => {
const invoice = await Invoice.findById(id);
if (!invoice) {
throw new NotFoundError('Rechnung nicht gefunden');
}
if (invoice.userId !== context.user.id && !context.user.isAdmin) {
throw new ForbiddenError('Zugriff verweigert');
}
return invoice;
}
}
};
// SICHER: Dateizugriff mit Eigentümerschaft
app.get('/files/:fileId', authenticate, async (req, res) => {
const file = await File.findById(req.params.fileId);
if (!file) {
return res.status(404).json({ error: 'Datei nicht gefunden' });
}
// Eigentümerschaft oder Freigabe prüfen
if (file.ownerId !== req.user.id &&
!file.sharedWith.includes(req.user.id)) {
return res.status(403).json({ error: 'Zugriff verweigert' });
}
res.sendFile(file.path);
});
Ausgenutzt in der Praxis
First American Financial (2019)
885 Millionen Dokumente mit sensiblen persönlichen und finanziellen Daten wurden aufgrund sequentieller Dokument-IDs ohne Autorisierungsprüfungen exponiert. Jeder könnte auf jedes Dokument zugreifen, indem er die ID inkrementierte.
Parler-Daten-Scraping (2021)
Parlers API verwendete sequentielle Post-IDs ohne Autorisierung, was Forschern ermöglichte, den gesamten Plattforminhalt einschließlich gelöschter Posts und Benutzerdaten herunterzuladen.
Facebook-Foto-Exposition (2018)
Ein Fehler ermöglichte Apps den Zugriff auf Fotos, die Benutzer hochgeladen aber nie geteilt hatten, und betraf 6,8 Millionen Benutzer aufgrund unsachgemäßer Autorisierung auf Foto-IDs.
Tools zum Testen/Ausnutzen
- Burp Suite — ID-Parameter abfangen und modifizieren.
- Autorize — Burp-Erweiterung für IDOR-Tests.
- OWASP ZAP — automatisierte IDOR-Erkennung.
- ffuf — ID-Enumeration und Fuzzing.
CVE-Beispiele
- CVE-2021-39165 — Cachet IDOR-Schwachstelle.
- CVE-2020-36193 — Archive_Tar IDOR.
- CVE-2019-12164 — Zoho ManageEngine IDOR.
Referenzen
- MITRE. "CWE-639: Authorization Bypass Through User-Controlled Key." https://cwe.mitre.org/data/definitions/639.html
- OWASP. "Insecure Direct Object References." https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/05-Authorization_Testing/04-Testing_for_Insecure_Direct_Object_References