Erzeugung vorhersagbarer Zahlen oder Identifikatoren

Beschreibung

Erzeugung vorhersagbarer Zahlen oder Identifikatoren tritt auf, wenn Software Identifikatoren, Sequenznummern oder andere Werte generiert, die unvorhersagbar sein sollten, aber von einem Angreifer vorhergesagt werden können. Dies umfasst sequenzielle IDs, zeitstempelbasierte Identifikatoren, vorhersagbare Dateinamen, erratbare URLs und andere Werte, bei denen Unvorhersagbarkeit eine Sicherheitsanforderung ist. Anders als bei schwachen PRNGs kann diese Schwäche auch ohne explizite Randomisierung auftreten, wenn vorhersagbare Muster verwendet werden.

Risiko

Vorhersagbare Identifikatoren ermöglichen Enumerationsangriffe, Session Hijacking und Zugriff auf unautorisierte Ressourcen. Sequenzielle Benutzer-IDs ermöglichen Angreifern, gültige Konten zu erraten. Vorhersagbare Dateinamen legen hochgeladene Inhalte offen. Zeitbasierte Tokens können erraten werden, wenn der Erstellungszeitpunkt bekannt ist. Datenbank-IDs in URLs ermöglichen horizontale Rechteeskalation durch IDOR-Angriffe. Diese Schwäche wird häufig in Webanwendungen ausgenutzt, bei denen Ressourcenidentifikatoren in URLs oder APIs offengelegt werden.

Lösung

Verwenden Sie kryptographisch zufällige Werte für sicherheitssensible Identifikatoren. Ersetzen Sie sequenzielle IDs durch UUIDs (Version 4) für die externe Darstellung. Implementieren Sie indirekte Referenzkarten, die interne IDs in zufällige externe Tokens übersetzen. Vermeiden Sie die direkte Offenlegung von Datenbank-Auto-Increment-IDs. Verwenden Sie Zeitstempel nicht allein als Identifikatoren. Wenden Sie Zugriffskontrollen unabhängig von der Vorhersagbarkeit der Identifikatoren an -- behandeln Sie Unvorhersagbarkeit als Defense-in-Depth, nicht als primäre Zugriffskontrolle.

Häufige Auswirkungen

AuswirkungDetails
ZugriffskontrolleBereich: Unautorisierter Zugriff

Angreifer greifen auf Ressourcen zu, indem sie deren Identifikatoren erraten.
VertraulichkeitBereich: Informationspreisgabe

Enumeration offenbart gültige Benutzer, Dateien oder Ressourcen.
IntegritätBereich: Datenmanipulation

Vorhersagbare Identifikatoren ermöglichen unautorisierte Änderungen.

Beispielcode und Lösung

Verwundbarer Code

# VERWUNDBAR: Sequenzielle IDs offengelegt
from flask import Flask, jsonify

# Auto-Increment-IDs in URLs
@app.route('/api/users/<int:user_id>')
def get_user(user_id):
    # Sequenzielle IDs ermöglichen Enumeration!
    # Angreifer versucht /api/users/1, /api/users/2, usw.
    user = User.query.get(user_id)
    if user:
        return jsonify(user.to_dict())
    return jsonify({'error': 'Not found'}), 404

# VERWUNDBAR: Vorhersagbare Dateinamen
@app.route('/upload', methods=['POST'])
def upload_file():
    file = request.files['file']
    # Zeitstempelbasierter Name - vorhersagbar!
    filename = f"{int(time.time())}_{file.filename}"
    file.save(f"/uploads/{filename}")
    return jsonify({'url': f'/files/{filename}'})

# VERWUNDBAR: Vorhersagbare Token-Generierung
def generate_password_reset_token(user_id):
    # Token basiert auf Zeitstempel und user_id - erratbar!
    timestamp = int(time.time())
    return f"{user_id}_{timestamp}"

# VERWUNDBAR: Sequenzielle Bestellnummern
order_counter = 0

def create_order(user_id, items):
    global order_counter
    order_counter += 1
    # Sequenzielle Bestell-ID - Wettbewerber können Volumen verfolgen!
    order_id = f"ORD-{order_counter:08d}"
    return order_id

# VERWUNDBAR: Vorhersagbare Session-IDs
def create_session(user_id):
    # Session-ID aus user_id und Zeitstempel
    session_id = hashlib.md5(f"{user_id}{time.time()}".encode()).hexdigest()
    return session_id

# VERWUNDBAR: Auto-Increment-Rechnungsnummern
invoice_number = 1000

def generate_invoice_number():
    global invoice_number
    invoice_number += 1
    return f"INV-{invoice_number}"  # Offenbart Geschäftsvolumen!
// VERWUNDBAR: Java mit vorhersagbaren Identifikatoren
@RestController
public class VulnerableController {

    // Sequenzielle IDs in API offengelegt
    @GetMapping("/api/documents/{id}")
    public Document getDocument(@PathVariable Long id) {
        // Auto-Increment-ID aus Datenbank
        // Angreifer enumerieren alle Dokumente
        return documentRepository.findById(id).orElse(null);
    }

    // VERWUNDBAR: Vorhersagbare Dateinamen
    @PostMapping("/upload")
    public String uploadFile(@RequestParam MultipartFile file) {
        String filename = System.currentTimeMillis() + "_" + file.getOriginalFilename();
        // Vorhersagbarer zeitstempelbasierter Name
        saveFile(filename, file);
        return "/files/" + filename;
    }
}

// VERWUNDBAR: Sequenzielle ID-Generierung
@Entity
public class VulnerableEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;  // Sequenziell, vorhersagbar

    // Externe API legt dies direkt offen
}

// VERWUNDBAR: Vorhersagbare Referenznummern
public class OrderService {

    private AtomicLong counter = new AtomicLong(0);

    public String generateOrderNumber() {
        // Sequenziell - Wettbewerber verfolgen Bestellvolumen
        return String.format("ORD-%010d", counter.incrementAndGet());
    }

    public String generateResetToken(Long userId) {
        // Zeitstempel + Benutzer-ID = vorhersagbar
        return userId + "-" + System.currentTimeMillis();
    }
}
// VERWUNDBAR: Express mit vorhersagbaren Identifikatoren
const express = require('express');

// Sequenzielle IDs in API
app.get('/api/profiles/:id', (req, res) => {
    // Auto-Increment-Datenbank-ID
    // Angreifer enumerieren: /api/profiles/1, /api/profiles/2...
    Profile.findById(req.params.id).then(profile => {
        res.json(profile);
    });
});

// VERWUNDBAR: Vorhersagbare Datei-Uploads
app.post('/upload', upload.single('file'), (req, res) => {
    // Zeitstempelbasierter Dateiname
    const filename = `${Date.now()}_${req.file.originalname}`;
    fs.rename(req.file.path, `uploads/${filename}`, () => {
        res.json({ url: `/files/${filename}` });
    });
});

// VERWUNDBAR: Vorhersagbare Tokens
function generateResetToken(userId) {
    // Nur Zeitstempel und Benutzer-ID
    return `${userId}_${Date.now()}`;
}

// VERWUNDBAR: Sequenzielle Bestellnummern
let orderCounter = 0;
function generateOrderNumber() {
    orderCounter++;
    return `ORD-${String(orderCounter).padStart(8, '0')}`;
}

// VERWUNDBAR: Datenbank-IDs in GraphQL
const resolvers = {
    Query: {
        // Legt sequenzielle Datenbank-IDs offen
        user: (_, { id }) => User.findById(id),
        order: (_, { id }) => Order.findById(id)
    }
};

Sichere Lösung

# SICHER: Unvorhersagbare Identifikatoren
import secrets
import uuid
import hashlib
from flask import Flask, jsonify

# SICHER: UUIDs für externe Referenzen verwenden
class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)  # Nur intern
    public_id = db.Column(db.String(36), unique=True, default=lambda: str(uuid.uuid4()))

@app.route('/api/users/<public_id>')
def get_user_safe(public_id):
    # UUID kann nicht enumeriert werden
    user = User.query.filter_by(public_id=public_id).first()
    if user:
        return jsonify(user.to_dict())
    return jsonify({'error': 'Not found'}), 404

# SICHER: Zufällige Dateinamen
@app.route('/upload', methods=['POST'])
def upload_file_safe():
    file = request.files['file']

    # Zufälliger Dateiname mit erhaltener Erweiterung
    ext = os.path.splitext(file.filename)[1]
    random_name = secrets.token_urlsafe(32) + ext

    file.save(f"/uploads/{random_name}")

    # Originalnamen in Datenbank speichern
    FileUpload.create(
        random_name=random_name,
        original_name=file.filename,
        user_id=current_user.id
    )

    return jsonify({'url': f'/files/{random_name}'})

# SICHER: Kryptographische Tokens
def generate_password_reset_token_safe(user_id):
    """Erzeugt unvorhersagbaren Reset-Token."""
    token = secrets.token_urlsafe(32)

    # Hash des Tokens speichern, nicht den Token selbst
    token_hash = hashlib.sha256(token.encode()).hexdigest()
    PasswordReset.create(
        user_id=user_id,
        token_hash=token_hash,
        expires_at=datetime.utcnow() + timedelta(hours=1)
    )

    return token

# SICHER: Zufällige Bestellreferenzen
def create_order_safe(user_id, items):
    # Zufällige Bestellreferenz
    order_ref = secrets.token_urlsafe(16).upper()

    order = Order(
        user_id=user_id,
        reference=order_ref,
        items=items
    )
    db.session.add(order)
    db.session.commit()

    return order_ref

# SICHER: Indirekte Referenzkarte
class IndirectReferenceMap:
    """Bildet interne IDs auf zufällige externe Tokens ab."""

    def __init__(self):
        self.internal_to_external = {}
        self.external_to_internal = {}

    def get_external_id(self, internal_id, create=True):
        """Externe ID für interne ID holen oder erstellen."""
        if internal_id in self.internal_to_external:
            return self.internal_to_external[internal_id]

        if not create:
            return None

        external_id = secrets.token_urlsafe(16)
        self.internal_to_external[internal_id] = external_id
        self.external_to_internal[external_id] = internal_id
        return external_id

    def get_internal_id(self, external_id):
        """Externe ID in interne ID auflösen."""
        return self.external_to_internal.get(external_id)

# Verwendung mit Session-gebundener Karte
@app.before_request
def setup_reference_map():
    if 'ref_map' not in session:
        session['ref_map'] = IndirectReferenceMap()

@app.route('/api/documents/<external_id>')
def get_document_safe(external_id):
    ref_map = session.get('ref_map')
    internal_id = ref_map.get_internal_id(external_id)

    if not internal_id:
        return jsonify({'error': 'Not found'}), 404

    document = Document.query.get(internal_id)

    # Auch Zugriffskontrolle prüfen!
    if document.owner_id != current_user.id:
        return jsonify({'error': 'Not authorized'}), 403

    return jsonify(document.to_dict())
// SICHER: Java mit unvorhersagbaren Identifikatoren
@Entity
public class SecureEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;  // Nur intern, wird nie offengelegt

    @Column(unique = true, nullable = false)
    private String publicId = UUID.randomUUID().toString();

    // Nur publicId in der API offenlegen
    public String getPublicId() {
        return publicId;
    }
}

@RestController
public class SecureController {

    @GetMapping("/api/documents/{publicId}")
    public ResponseEntity<Document> getDocument(
            @PathVariable String publicId,
            Authentication auth) {

        // Nach UUID suchen
        Document doc = documentRepository.findByPublicId(publicId)
            .orElseThrow(() -> new NotFoundException("Document not found"));

        // Trotzdem Autorisierung prüfen!
        if (!doc.getOwnerId().equals(auth.getName())) {
            throw new AccessDeniedException("Not authorized");
        }

        return ResponseEntity.ok(doc);
    }

    @PostMapping("/upload")
    public ResponseEntity<UploadResponse> uploadFile(
            @RequestParam MultipartFile file,
            Authentication auth) {

        // Zufälliger Dateiname
        String randomName = UUID.randomUUID().toString();
        String extension = getExtension(file.getOriginalFilename());
        String storedName = randomName + extension;

        // Datei speichern
        fileService.store(storedName, file);

        // Metadaten speichern
        FileMetadata metadata = new FileMetadata();
        metadata.setStoredName(storedName);
        metadata.setOriginalName(file.getOriginalFilename());
        metadata.setOwnerId(auth.getName());
        fileRepository.save(metadata);

        return ResponseEntity.ok(new UploadResponse(randomName));
    }
}

// SICHER: Sichere Token-Generierung
@Service
public class TokenService {

    public String generateResetToken(Long userId) {
        byte[] randomBytes = new byte[32];
        new SecureRandom().nextBytes(randomBytes);
        String token = Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);

        // Hash speichern
        String tokenHash = DigestUtils.sha256Hex(token);
        resetTokenRepository.save(new ResetToken(userId, tokenHash, Instant.now().plusSeconds(3600)));

        return token;
    }
}

// SICHER: Zufällige Bestellreferenz
@Service
public class OrderService {

    private SecureRandom secureRandom = new SecureRandom();

    public String generateOrderReference() {
        byte[] bytes = new byte[8];
        secureRandom.nextBytes(bytes);
        // Zufällige Referenz wie "ORD-A1B2C3D4"
        return "ORD-" + Hex.encodeHexString(bytes).toUpperCase().substring(0, 8);
    }
}
// SICHER: Node.js mit unvorhersagbaren Identifikatoren
const crypto = require('crypto');
const { v4: uuidv4 } = require('uuid');

// Modell mit öffentlicher UUID
class User {
    constructor(data) {
        this.id = data.id;  // Internes Auto-Increment
        this.publicId = data.publicId || uuidv4();  // Externe UUID
    }
}

// SICHER: API mit UUID-Suche
app.get('/api/profiles/:publicId', async (req, res) => {
    const { publicId } = req.params;

    // Nach UUID suchen - nicht enumerierbar
    const profile = await Profile.findOne({ publicId });

    if (!profile) {
        return res.status(404).json({ error: 'Not found' });
    }

    // Trotzdem Autorisierung prüfen!
    if (profile.userId !== req.user.id && !req.user.isAdmin) {
        return res.status(403).json({ error: 'Not authorized' });
    }

    res.json(profile);
});

// SICHER: Zufällige Dateinamen
app.post('/upload', upload.single('file'), async (req, res) => {
    const ext = path.extname(req.file.originalname);
    const randomName = crypto.randomBytes(32).toString('hex') + ext;

    await fs.promises.rename(req.file.path, `uploads/${randomName}`);

    // Metadaten speichern
    await FileMetadata.create({
        storedName: randomName,
        originalName: req.file.originalname,
        userId: req.user.id,
        publicId: uuidv4()
    });

    res.json({ fileId: randomName.slice(0, 32) });
});

// SICHER: Kryptographische Token-Generierung
function generateResetToken(userId) {
    const token = crypto.randomBytes(32).toString('hex');

    // Hash speichern
    const tokenHash = crypto.createHash('sha256').update(token).digest('hex');

    ResetToken.create({
        userId,
        tokenHash,
        expiresAt: new Date(Date.now() + 3600000)
    });

    return token;
}

// SICHER: Zufällige Bestellreferenz
function generateOrderReference() {
    const bytes = crypto.randomBytes(6);
    return 'ORD-' + bytes.toString('hex').toUpperCase();
}

// SICHER: GraphQL mit öffentlichen IDs
const resolvers = {
    Query: {
        user: async (_, { publicId }, context) => {
            const user = await User.findOne({ publicId });
            if (!user) throw new NotFoundError('User not found');

            // Autorisierungsprüfung
            if (user.id !== context.userId && !context.isAdmin) {
                throw new ForbiddenError('Not authorized');
            }

            return user;
        }
    }
};

// SICHER: Indirekter Referenz-Service
class IndirectReferenceService {
    constructor() {
        this.cache = new Map();  // In Produktion Redis verwenden
    }

    createExternalRef(internalId, userId) {
        const key = `${userId}:${internalId}`;
        if (this.cache.has(key)) {
            return this.cache.get(key);
        }

        const externalRef = crypto.randomBytes(16).toString('hex');
        this.cache.set(key, externalRef);
        this.cache.set(externalRef, { internalId, userId });
        return externalRef;
    }

    resolveRef(externalRef, userId) {
        const data = this.cache.get(externalRef);
        if (!data || data.userId !== userId) {
            return null;
        }
        return data.internalId;
    }
}

Ausgenutzt in der Praxis

IDOR-Schwachstellen

Unzählige Datenverstöße sind durch Enumeration sequenzieller IDs in URLs aufgetreten und haben Benutzerdaten, Dokumente und Bestellungen offengelegt.

Vorhersagbare Passwort-Reset-Tokens

Reset-Tokens basierend auf Zeitstempeln oder Benutzer-IDs wurden zur Kontoübernahme ausgenutzt.

Geschäftsinformations-Leckage

Sequenzielle Rechnungs- und Bestellnummern haben Geschäftsvolumen und Wachstumsraten an Wettbewerber verraten.


Tools zum Testen und Ausnutzen

  • Burp Suite Intruder -- sequenzielle IDs enumerieren.

  • ffuf -- nach vorhersagbaren Identifikatoren fuzzen.

  • OWASP ZAP -- automatisierte Enumerationstests.

  • Benutzerdefinierte Skripte für ID-Vorhersage.


CVE-Beispiele

  • CVE-2019-11358 -- Probleme mit vorhersagbaren Identifikatoren.

  • CVE-2021-22893 -- Schwachstellen durch sequenzielle IDs.

  • Zahlreiche anwendungsspezifische IDOR-Schwachstellen.


Referenzen

  1. MITRE. "CWE-340: Generation of Predictable Numbers or Identifiers." https://cwe.mitre.org/data/definitions/340.html

  2. OWASP. "Insecure Direct Object References." https://owasp.org/www-project-web-security-testing-guide/