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
| Impact | Details |
|---|---|
| Access Control | Scope: Unauthorized Access Attackers access resources by guessing their identifiers. |
| Confidentiality | Scope: Information Disclosure Enumeration reveals valid users, files, or resources. |
| Integrity | Scope: 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
-
MITRE. "CWE-340: Generation of Predictable Numbers or Identifiers." https://cwe.mitre.org/data/definitions/340.html
-
OWASP. "Insecure Direct Object References." https://owasp.org/www-project-web-security-testing-guide/