Predictable from Observable State
Description
Predictable from Observable State is a vulnerability that occurs when numbers or identifiers can be predicted by an attacker who observes system or network conditions such as time, process ID, network statistics, or other accessible state information. Unlike purely sequential identifiers or those derived from fixed seeds, these values incorporate observable system state that appears to add randomness but remains predictable to attackers who can observe or estimate that state. Common patterns include using system time (even with high precision), process or thread IDs, memory addresses, network packet sequences, or combinations of these values. The apparent complexity of the generation scheme creates a false sense of security.
Risk
Observable state-based identifiers create vulnerabilities where attackers can narrow the prediction space dramatically by gathering system information. An attacker on the same network can observe timing patterns. Process IDs are limited to 16-bit ranges and can be discovered through various side channels. Memory addresses, while subject to ASLR, provide limited additional entropy. Timestamps, even at microsecond precision, can be estimated by attackers who know when operations occurred. This enables session hijacking when session tokens incorporate login time, file access when filenames include creation timestamps, DNS spoofing when query IDs derive from system state, and authentication bypass when tokens can be reconstructed from observable values. The vulnerability is particularly dangerous because developers may believe the identifier is sufficiently random while attackers can reconstruct it.
Solution
Never derive security-sensitive identifiers from observable system state. Use cryptographically secure random number generators that don't depend on predictable inputs. For session tokens, generate 128+ bits of entropy from CSPRNGs. For file names requiring uniqueness, combine random values with timestamps rather than using timestamps alone. Implement indirect references that map external identifiers (shown to users) to internal identifiers (used by the system). If system state must be incorporated for operational reasons (such as logging or debugging), ensure the security-critical component is still derived from a CSPRNG. Conduct threat modeling to identify what system state attackers can observe and ensure identifier generation doesn't rely on that information.
Common Consequences
| Impact | Details |
|---|---|
| Access Control | Scope: Access Control Attackers who can observe or estimate system state can predict authentication tokens, session IDs, and access credentials, bypassing authentication and authorization. |
| Confidentiality | Scope: Confidentiality Predictable file names or resource identifiers based on observable state enable unauthorized access to files and data uploaded or created by other users. |
| Integrity | Scope: Integrity Predictable transaction IDs or sequence numbers enable request forgery, replay attacks, and unauthorized modifications. |
Example Code
Vulnerable Code (Python/Java)
The following examples demonstrate identifiers predictable from observable state:
# Vulnerable: Identifiers based on observable state
import time
import os
import socket
# Vulnerable: Timestamp-based session token
def vulnerable_session_token():
# Vulnerable: Time is observable by attacker
timestamp = int(time.time() * 1000000) # Microseconds
return format(timestamp, '016x')
# Vulnerable: PID + time combination
def vulnerable_combined_token():
# Vulnerable: Both values are observable
pid = os.getpid()
timestamp = int(time.time())
return f"{pid:05d}-{timestamp}"
# Vulnerable: Network interface-based seed
def vulnerable_network_seed():
import random
# Vulnerable: Hostname is discoverable
hostname = socket.gethostname()
random.seed(hash(hostname) ^ int(time.time()))
return random.getrandbits(64)
# Vulnerable: Memory address as identifier
def vulnerable_address_id():
obj = object()
# Vulnerable: Address space is limited, ASLR partial
return format(id(obj), '016x')
# Vulnerable: Thread ID incorporation
import threading
def vulnerable_thread_token():
# Vulnerable: Thread IDs are enumerable
thread_id = threading.current_thread().ident
timestamp = int(time.time() * 1000)
return f"T{thread_id:08x}-{timestamp}"
# Vulnerable: System uptime-based
def vulnerable_uptime_token():
# Vulnerable: Uptime can be estimated
with open('/proc/uptime', 'r') as f:
uptime = float(f.read().split()[0])
return format(int(uptime * 1000000), '016x')
# Vulnerable: Login time in reset token
def vulnerable_reset_token(user_id, login_time):
# Vulnerable: Both values are known or observable
import hashlib
data = f"{user_id}:{login_time}"
return hashlib.sha256(data.encode()).hexdigest()[:32]
// Vulnerable: Identifiers based on observable state in Java
import java.util.Random;
import java.net.InetAddress;
public class VulnerableObservableState {
// Vulnerable: System.currentTimeMillis() based
public String vulnerableTimeToken() {
// Vulnerable: Time is observable
return Long.toHexString(System.currentTimeMillis());
}
// Vulnerable: nanoTime-based (appears more random)
public String vulnerableNanoToken() {
// Vulnerable: Still based on observable time
return Long.toHexString(System.nanoTime());
}
// Vulnerable: Thread ID incorporation
public String vulnerableThreadToken() {
// Vulnerable: Thread IDs are limited and observable
long threadId = Thread.currentThread().getId();
long time = System.currentTimeMillis();
return String.format("%08x%016x", threadId, time);
}
// Vulnerable: Object hashcode
public String vulnerableHashToken() {
Object obj = new Object();
// Vulnerable: Hashcode is deterministic from memory layout
return Integer.toHexString(obj.hashCode());
}
// Vulnerable: Runtime memory state
public String vulnerableMemoryToken() {
Runtime runtime = Runtime.getRuntime();
// Vulnerable: Memory stats are queryable
long free = runtime.freeMemory();
long total = runtime.totalMemory();
return Long.toHexString(free ^ total ^ System.currentTimeMillis());
}
// Vulnerable: Host-based seed
public String vulnerableHostToken() throws Exception {
// Vulnerable: Hostname and IP are discoverable
InetAddress addr = InetAddress.getLocalHost();
String host = addr.getHostAddress();
return Integer.toHexString(host.hashCode());
}
// Vulnerable: Combining weak observable sources
public String vulnerableCombinedToken() {
long seed = System.currentTimeMillis() ^
Thread.currentThread().getId() ^
Runtime.getRuntime().freeMemory();
Random rand = new Random(seed);
return Long.toHexString(rand.nextLong());
}
}
// Vulnerable: Identifiers based on observable state in C
#include <stdio.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <pthread.h>
// Vulnerable: Time-based token
void vulnerable_time_token(char *buffer, size_t len) {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
// Vulnerable: Time is observable
snprintf(buffer, len, "%lx%09lx",
(long)ts.tv_sec, ts.tv_nsec);
}
// Vulnerable: PID-based identifier
void vulnerable_pid_token(char *buffer, size_t len) {
// Vulnerable: PID is enumerable (15-16 bits)
snprintf(buffer, len, "P%05d-%ld", getpid(), time(NULL));
}
// Vulnerable: Thread ID incorporation
void vulnerable_thread_token(char *buffer, size_t len) {
pthread_t tid = pthread_self();
// Vulnerable: Thread IDs are observable
snprintf(buffer, len, "T%lx-%ld", (unsigned long)tid, time(NULL));
}
// Vulnerable: Stack address entropy
void vulnerable_stack_token(char *buffer, size_t len) {
int stack_var;
// Vulnerable: ASLR provides limited entropy
snprintf(buffer, len, "%p-%ld", (void*)&stack_var, time(NULL));
}
// Vulnerable: Boot time based
void vulnerable_boot_token(char *buffer, size_t len) {
FILE *f = fopen("/proc/uptime", "r");
double uptime;
fscanf(f, "%lf", &uptime);
fclose(f);
// Vulnerable: Uptime can be estimated
snprintf(buffer, len, "%016lx", (unsigned long)(uptime * 1000000));
}
// Vulnerable: Network statistics based
void vulnerable_network_token(char *buffer, size_t len) {
FILE *f = fopen("/proc/net/dev", "r");
long bytes_rx = 0;
// Parse network stats (simplified)
// Vulnerable: Network counters are observable on same network
snprintf(buffer, len, "%016lx", bytes_rx ^ time(NULL));
fclose(f);
}
Fixed Code (Python/Java)
# Fixed: Identifiers not based on observable state
import secrets
import os
import uuid
# Fixed: Cryptographic random session token
def secure_session_token():
# Fixed: Pure random from CSPRNG
return secrets.token_hex(16)
# Fixed: No PID or time dependency
def secure_token():
# Fixed: System state not used
return secrets.token_urlsafe(32)
# Fixed: Random file name
def secure_filename(extension):
# Fixed: Random, not time-based
random_part = secrets.token_hex(16)
return f"file_{random_part}{extension}"
# Fixed: UUID for unique identifiers
def secure_uuid_token():
# Fixed: UUID4 uses secure random
return str(uuid.uuid4())
# Fixed: If operational info needed, keep separate from security
def secure_with_context(operation_name):
import time
# Operational prefix (not security-relevant)
prefix = f"{operation_name}_{int(time.time())}"
# Security token is purely random
token = secrets.token_hex(16)
return f"{prefix}_{token}"
# Fixed: Secure reset token (no user info)
def secure_reset_token():
# Fixed: Pure random, not derived from observable state
return secrets.token_urlsafe(32)
# Fixed: Thread-safe secure token
import threading
_lock = threading.Lock()
def secure_thread_safe_token():
# Fixed: Thread safety without using thread ID
with _lock:
return secrets.token_hex(16)
// Fixed: Identifiers not based on observable state in Java
import java.security.SecureRandom;
import java.util.UUID;
import java.util.Base64;
public class SecureNonObservable {
private static final SecureRandom secureRandom = new SecureRandom();
// Fixed: Pure random token
public String secureToken() {
// Fixed: Not based on time or system state
byte[] bytes = new byte[16];
secureRandom.nextBytes(bytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}
// Fixed: No nanoTime dependency
public String secureTimelessToken() {
// Fixed: Random only
byte[] bytes = new byte[32];
secureRandom.nextBytes(bytes);
return bytesToHex(bytes);
}
// Fixed: No thread ID in token
public String secureThreadSafeToken() {
// Fixed: Thread ID not used
byte[] bytes = new byte[16];
secureRandom.nextBytes(bytes);
return bytesToHex(bytes);
}
// Fixed: UUID instead of hashcode
public String secureUniqueId() {
// Fixed: UUID4 uses SecureRandom
return UUID.randomUUID().toString();
}
// Fixed: No memory state dependency
public String secureMemoryIndependentToken() {
// Fixed: Not based on runtime state
byte[] bytes = new byte[32];
secureRandom.nextBytes(bytes);
return bytesToHex(bytes);
}
// Fixed: No host dependency
public String secureHostIndependentToken() {
// Fixed: Not based on hostname or IP
byte[] bytes = new byte[16];
secureRandom.nextBytes(bytes);
return bytesToHex(bytes);
}
// Fixed: Combining operational context with secure random
public String secureWithContext(String context) {
// Context is for logging/debugging only
String secureToken = UUID.randomUUID().toString();
// Don't expose context in security-critical uses
return secureToken;
}
private String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}
// Fixed: Identifiers not based on observable state in C
#include <openssl/rand.h>
#include <stdio.h>
#include <string.h>
// Fixed: Pure random token
int secure_token(char *buffer, size_t len) {
unsigned char random_bytes[16];
// Fixed: Cryptographic random only
if (RAND_bytes(random_bytes, sizeof(random_bytes)) != 1) {
return -1;
}
for (size_t i = 0; i < sizeof(random_bytes) && i * 2 < len - 1; i++) {
sprintf(buffer + (i * 2), "%02x", random_bytes[i]);
}
return 0;
}
// Fixed: No PID dependency
int secure_session_id(char *buffer, size_t len) {
// Fixed: Not based on PID or time
unsigned char random_bytes[32];
if (RAND_bytes(random_bytes, sizeof(random_bytes)) != 1) {
return -1;
}
for (size_t i = 0; i < sizeof(random_bytes) && i * 2 < len - 1; i++) {
sprintf(buffer + (i * 2), "%02x", random_bytes[i]);
}
return 0;
}
// Fixed: Secure temp file name
int secure_temp_filename(char *buffer, size_t len) {
unsigned char random_bytes[8];
// Fixed: Random, not time-based
if (RAND_bytes(random_bytes, sizeof(random_bytes)) != 1) {
return -1;
}
char hex[17];
for (int i = 0; i < 8; i++) {
sprintf(hex + (i * 2), "%02x", random_bytes[i]);
}
snprintf(buffer, len, "/tmp/secure_%s.tmp", hex);
return 0;
}
// Fixed: UUID-like identifier
int secure_uuid(char *buffer, size_t len) {
unsigned char random_bytes[16];
if (RAND_bytes(random_bytes, sizeof(random_bytes)) != 1) {
return -1;
}
// Format as UUID
snprintf(buffer, len,
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
random_bytes[0], random_bytes[1], random_bytes[2], random_bytes[3],
random_bytes[4], random_bytes[5], random_bytes[6], random_bytes[7],
random_bytes[8], random_bytes[9], random_bytes[10], random_bytes[11],
random_bytes[12], random_bytes[13], random_bytes[14], random_bytes[15]);
return 0;
}
The fix uses cryptographically secure random generation independent of observable system state.
Exploited in the Wild
E-commerce Timestamp Authentication (CVE-2024-48445)
An e-commerce application used guessable timestamps in weak authentication mechanisms, enabling unauthorized access.
Mail Server Predictable Filenames (CVE-2002-0389)
A mail server stored messages with predictable filenames based on time and sequence, allowing unauthorized access.
Tools to Test/Exploit
-
Burp Suite — Analyze patterns in identifiers over time.
-
Timing analysis tools — Correlate observed tokens with system time.
-
Process enumeration — Discover PID patterns in multi-process systems.
CVE Examples
-
CVE-2024-48445 — Timestamp-based weak authentication.
-
CVE-2002-0389 — Predictable mail filenames.
-
CVE-2000-0335 — DNS resolver predictable IDs.
References
-
MITRE Corporation. "CWE-341: Predictable from Observable State." Common Weakness Enumeration. https://cwe.mitre.org/data/definitions/341.html
-
OWASP Foundation. "Insecure Randomness." https://owasp.org/www-community/vulnerabilities/Insecure_Randomness
-
NIST. "Recommendation for Random Number Generation." SP 800-90A. https://csrc.nist.gov/publications/detail/sp/800-90a/rev-1/final