Generation of Predictable Numbers or Identifiers

Description

Generation of Predictable Numbers or Identifiers occurs when software generates identifiers, sequence numbers, or other values that should be unpredictable but can be predicted by an attacker. This includes sequential IDs, timestamp-based identifiers, predictable file names, guessable URLs, and other values where unpredictability is a security requirement. Unlike weak PRNGs, this weakness can occur even without explicit randomization when predictable patterns are used.

Risk

Predictable identifiers enable enumeration attacks, session hijacking, and access to unauthorized resources. Sequential user IDs allow attackers to guess valid accounts. Predictable file names expose uploaded content. Time-based tokens can be guessed if the creation time is known. Database IDs in URLs enable horizontal privilege escalation through IDOR attacks. This weakness is commonly exploited in web applications where resource identifiers are exposed in URLs or APIs.

Solution

Use cryptographically random values for security-sensitive identifiers. Replace sequential IDs with UUIDs (version 4) for external exposure. Implement indirect reference maps that translate internal IDs to random external tokens. Avoid exposing database auto-increment IDs directly. Don't use timestamps alone as identifiers. Apply access controls regardless of identifier predictability—treat unpredictability as defense in depth, not primary access control.

Common Consequences

ImpactDetails
Access ControlScope: Unauthorized Access

Attackers access resources by guessing their identifiers.
ConfidentialityScope: Information Disclosure

Enumeration reveals valid users, files, or resources.
IntegrityScope: Data Manipulation

Predictable identifiers enable unauthorized modifications.

Example Code + Solution Code

Vulnerable Code

# VULNERABLE: Sequential IDs exposed
from flask import Flask, jsonify

# Auto-increment IDs in URLs
@app.route('/api/users/<int:user_id>')
def get_user(user_id):
    # Sequential IDs allow enumeration!
    # Attacker tries /api/users/1, /api/users/2, etc.
    user = User.query.get(user_id)
    if user:
        return jsonify(user.to_dict())
    return jsonify({'error': 'Not found'}), 404

# VULNERABLE: Predictable file names
@app.route('/upload', methods=['POST'])
def upload_file():
    file = request.files['file']
    # Timestamp-based name - predictable!
    filename = f"{int(time.time())}_{file.filename}"
    file.save(f"/uploads/{filename}")
    return jsonify({'url': f'/files/{filename}'})

# VULNERABLE: Predictable token generation
def generate_password_reset_token(user_id):
    # Token based on timestamp and user_id - guessable!
    timestamp = int(time.time())
    return f"{user_id}_{timestamp}"

# VULNERABLE: Sequential order numbers
order_counter = 0

def create_order(user_id, items):
    global order_counter
    order_counter += 1
    # Sequential order ID - competitors can track volume!
    order_id = f"ORD-{order_counter:08d}"
    return order_id

# VULNERABLE: Predictable session IDs
def create_session(user_id):
    # Session ID from user_id and timestamp
    session_id = hashlib.md5(f"{user_id}{time.time()}".encode()).hexdigest()
    return session_id

# VULNERABLE: Auto-increment invoice numbers
invoice_number = 1000

def generate_invoice_number():
    global invoice_number
    invoice_number += 1
    return f"INV-{invoice_number}"  # Reveals business volume!
// VULNERABLE: Java with predictable identifiers
@RestController
public class VulnerableController {

    // Sequential IDs exposed in API
    @GetMapping("/api/documents/{id}")
    public Document getDocument(@PathVariable Long id) {
        // Auto-increment ID from database
        // Attackers enumerate all documents
        return documentRepository.findById(id).orElse(null);
    }

    // VULNERABLE: Predictable file names
    @PostMapping("/upload")
    public String uploadFile(@RequestParam MultipartFile file) {
        String filename = System.currentTimeMillis() + "_" + file.getOriginalFilename();
        // Predictable timestamp-based name
        saveFile(filename, file);
        return "/files/" + filename;
    }
}

// VULNERABLE: Sequential ID generation
@Entity
public class VulnerableEntity {

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

    // External API exposes this directly
}

// VULNERABLE: Predictable reference numbers
public class OrderService {

    private AtomicLong counter = new AtomicLong(0);

    public String generateOrderNumber() {
        // Sequential - competitors track order volume
        return String.format("ORD-%010d", counter.incrementAndGet());
    }

    public String generateResetToken(Long userId) {
        // Timestamp + user ID = predictable
        return userId + "-" + System.currentTimeMillis();
    }
}
// VULNERABLE: Express with predictable identifiers
const express = require('express');

// Sequential IDs in API
app.get('/api/profiles/:id', (req, res) => {
    // Auto-increment database ID
    // Attackers enumerate: /api/profiles/1, /api/profiles/2...
    Profile.findById(req.params.id).then(profile => {
        res.json(profile);
    });
});

// VULNERABLE: Predictable file uploads
app.post('/upload', upload.single('file'), (req, res) => {
    // Timestamp-based filename
    const filename = `${Date.now()}_${req.file.originalname}`;
    fs.rename(req.file.path, `uploads/${filename}`, () => {
        res.json({ url: `/files/${filename}` });
    });
});

// VULNERABLE: Predictable tokens
function generateResetToken(userId) {
    // Just timestamp and user ID
    return `${userId}_${Date.now()}`;
}

// VULNERABLE: Sequential order numbers
let orderCounter = 0;
function generateOrderNumber() {
    orderCounter++;
    return `ORD-${String(orderCounter).padStart(8, '0')}`;
}

// VULNERABLE: Database IDs in GraphQL
const resolvers = {
    Query: {
        // Exposes sequential database IDs
        user: (_, { id }) => User.findById(id),
        order: (_, { id }) => Order.findById(id)
    }
};

Fixed Code

# SAFE: Unpredictable identifiers
import secrets
import uuid
import hashlib
from flask import Flask, jsonify

# SAFE: Use UUIDs for external references
class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)  # Internal only
    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 cannot be enumerated
    user = User.query.filter_by(public_id=public_id).first()
    if user:
        return jsonify(user.to_dict())
    return jsonify({'error': 'Not found'}), 404

# SAFE: Random file names
@app.route('/upload', methods=['POST'])
def upload_file_safe():
    file = request.files['file']

    # Random filename with preserved extension
    ext = os.path.splitext(file.filename)[1]
    random_name = secrets.token_urlsafe(32) + ext

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

    # Store original name in database
    FileUpload.create(
        random_name=random_name,
        original_name=file.filename,
        user_id=current_user.id
    )

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

# SAFE: Cryptographic tokens
def generate_password_reset_token_safe(user_id):
    """Generate unpredictable reset token."""
    token = secrets.token_urlsafe(32)

    # Store hash of token, not the token itself
    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

# SAFE: Random order references
def create_order_safe(user_id, items):
    # Random order reference
    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

# SAFE: Indirect reference map
class IndirectReferenceMap:
    """Map internal IDs to random external tokens."""

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

    def get_external_id(self, internal_id, create=True):
        """Get or create external ID for internal ID."""
        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):
        """Resolve external ID to internal ID."""
        return self.external_to_internal.get(external_id)

# Usage with session-scoped map
@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)

    # Also verify access control!
    if document.owner_id != current_user.id:
        return jsonify({'error': 'Not authorized'}), 403

    return jsonify(document.to_dict())
// SAFE: Java with unpredictable identifiers
@Entity
public class SecureEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;  // Internal only, never exposed

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

    // Expose only publicId in API
    public String getPublicId() {
        return publicId;
    }
}

@RestController
public class SecureController {

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

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

        // Still check authorization!
        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) {

        // Random filename
        String randomName = UUID.randomUUID().toString();
        String extension = getExtension(file.getOriginalFilename());
        String storedName = randomName + extension;

        // Store file
        fileService.store(storedName, file);

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

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

// SAFE: Secure token generation
@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);

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

        return token;
    }
}

// SAFE: Random order reference
@Service
public class OrderService {

    private SecureRandom secureRandom = new SecureRandom();

    public String generateOrderReference() {
        byte[] bytes = new byte[8];
        secureRandom.nextBytes(bytes);
        // Random reference like "ORD-A1B2C3D4"
        return "ORD-" + Hex.encodeHexString(bytes).toUpperCase().substring(0, 8);
    }
}
// SAFE: Node.js with unpredictable identifiers
const crypto = require('crypto');
const { v4: uuidv4 } = require('uuid');

// Model with public UUID
class User {
    constructor(data) {
        this.id = data.id;  // Internal auto-increment
        this.publicId = data.publicId || uuidv4();  // External UUID
    }
}

// SAFE: API with UUID lookup
app.get('/api/profiles/:publicId', async (req, res) => {
    const { publicId } = req.params;

    // Lookup by UUID - not enumerable
    const profile = await Profile.findOne({ publicId });

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

    // Still check authorization!
    if (profile.userId !== req.user.id && !req.user.isAdmin) {
        return res.status(403).json({ error: 'Not authorized' });
    }

    res.json(profile);
});

// SAFE: Random file names
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}`);

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

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

// SAFE: Cryptographic token generation
function generateResetToken(userId) {
    const token = crypto.randomBytes(32).toString('hex');

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

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

    return token;
}

// SAFE: Random order reference
function generateOrderReference() {
    const bytes = crypto.randomBytes(6);
    return 'ORD-' + bytes.toString('hex').toUpperCase();
}

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

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

            return user;
        }
    }
};

// SAFE: Indirect reference service
class IndirectReferenceService {
    constructor() {
        this.cache = new Map();  // In production, use Redis
    }

    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;
    }
}

Exploited in the Wild

IDOR Vulnerabilities

Countless data breaches have occurred through enumeration of sequential IDs in URLs, exposing user data, documents, and orders.

Predictable Password Reset Tokens

Reset tokens based on timestamps or user IDs have been exploited to take over accounts.

Business Intelligence Leakage

Sequential invoice and order numbers have revealed business volume and growth rates to competitors.


Tools to test/exploit

  • Burp Suite Intruder — enumerate sequential IDs.

  • ffuf — fuzz for predictable identifiers.

  • OWASP ZAP — automated enumeration testing.

  • Custom scripts for ID prediction.


CVE Examples

  • CVE-2019-11358 — Predictable identifier issues.

  • CVE-2021-22893 — Sequential ID vulnerabilities.

  • Numerous application-specific IDOR vulnerabilities.


References

  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/