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

AuswirkungDetails
VertraulichkeitBereich: Datenexposition

Unbefugter Zugriff auf persönliche Daten, Dokumente und Datensätze anderer Benutzer.
IntegritätBereich: Datenänderung

Fähigkeit, Daten anderer Benutzer durch Manipulation von Objektreferenzen zu modifizieren oder zu löschen.
PrivatsphäreBereich: 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


Referenzen

  1. MITRE. "CWE-639: Authorization Bypass Through User-Controlled Key." https://cwe.mitre.org/data/definitions/639.html
  2. 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