Skip to content

CWE-665: Improper Initialization

Overview

Improper initialization occurs when a variable, object, buffer, or resource is used before it has been given a defined, safe value. That unset state can hold leftover memory contents, a null or undefined reference, or a default that bypasses a security check. The resulting behavior is hard to reproduce, because it depends on whatever the runtime happened to leave behind.

Relationship to Other CWEs

CWE-665 is a Class-level child of the pillar CWE-664 (Improper Control of a Resource Through its Lifetime), alongside CWE-404 (Improper Resource Shutdown or Release) at the other end of the same lifetime - one page for the resource that was never set up, one for the resource that was never let go.

MITRE's mapping guidance for CWE-665 is Discouraged: it is too abstract to file a real finding against, and one of its children is nearly always the right number. None of them has a page here yet, so use this page for the guidance and one of these for the report. The ones matching what this page teaches:

MITRE lists two further children whose subject this page does not cover, named here so the mapping set is complete. Both are the right number for their own finding, and neither is served by the guidance below:

OWASP Classification

A10:2025 - Mishandling of Exceptional Conditions

Risk

Medium-High: Uninitialized state can leak memory contents to an attacker (information disclosure), leave a security flag in a permissive default (authentication or authorization bypass), produce a predictable or unseeded cryptographic key (weak crypto), or crash the process (denial of service). Impact depends entirely on what the uninitialized value controls.

Remediation Steps

Core Principle: Every variable, object field, and resource must be set to an explicit, safe value before it is read, rather than to whatever a compiler, allocator, or runtime happens to leave behind.

Trace the Data Path

  • Source: A declaration, allocation, or object construction that does not assign an initial value on every path.
  • Sink: The first read of that value - a conditional check, a buffer copy, a cryptographic operation, or a return statement.
  • Data Flow / Missing Controls: Identify any code path where the assignment can be skipped (an early branch, an exception, a partially run constructor) so the sink is reached with the value still unset.

Initialize Every Declaration with a Safe Default (Primary Defense)

  • Assign a value at the point of declaration rather than leaving it to be set later on some paths and not others.
  • For security-relevant flags, default to the deny/false state: an "authenticated" or "authorized" flag must start false rather than be left unset.
  • For pointers and references, initialize to null/None explicitly so a missed assignment fails fast instead of dereferencing garbage.
  • For objects, set every field to a safe value in the constructor, including fields only used on some code paths.
  • For buffers and arrays, zero-fill on allocation rather than trusting the allocator to hand back clean memory.

Initialize Cryptographic State Correctly

  • Seed random values only from a cryptographically secure source; never use a general-purpose (non-cryptographic) RNG for tokens, keys, or nonces.
  • Confirm keys, IVs, and other cryptographic parameters are generated or loaded before use, and fail loudly if they are not - do not silently proceed with a null or zero key.

Add Compiler and Static Analysis Checks (Defense in Depth)

  • Enable compiler warnings for uninitialized use and treat them as build failures.
  • Run static analysis and memory sanitizers regularly to catch paths that skip initialization.
  • At sinks that carry security meaning, add a guard that fails closed if the value is unset, as a backstop against a future code path that skips initialization.

Test the Fix

  • Exercise every branch that could previously skip initialization, including error and exception paths.
  • Confirm security flags default to the safe/denied state when no explicit assignment occurs.
  • Use a memory sanitizer or uninitialized-value detector to confirm no path reads unset memory.
  • Re-scan with the security scanner to confirm the finding is resolved.

Common Vulnerable Patterns

A security flag left unset on some path

// VULNERABLE - flag left unset on some paths
declare isVerified
if check_signature():
    isVerified = true
if isVerified:          // unset value may be truthy garbage - skips the check
    process()

Why this is vulnerable: the flag is assigned only on success, so the failure path does not set it to false - it leaves it at whatever the declaration produced. Where that is uninitialized memory the value is arbitrary and frequently non-zero, so the branch meant to represent "signature checked and valid" is entered on the strength of leftover data.

The direction of the failure is what matters. The default of an unset flag ought to be the safe answer, and here the unsafe answer is the one that requires nothing to happen. That is worth checking wherever a boolean carries a security decision: initialize it to the denying value at the declaration, so that every path that does not explicitly grant ends up denying, and a path added later inherits the safe default rather than the absence of one.

A buffer sent without being filled

// VULNERABLE - buffer never zeroed
declare buffer[1024]     // contains whatever memory held previously
send_response(buffer)    // may leak prior data to the caller

Why this is vulnerable: memory handed to a program is not blank. A stack buffer holds whatever the previous call left in that region and a heap allocation holds whatever the last owner of that block wrote, so the uninitialized bytes are earlier data from this same process, which is where the credentials, keys and other users' requests are.

Testing tends to hide it precisely because the leak is data-dependent. Early in a process's life the memory has often never been used and reads as zeroes, so a fresh run looks clean and the disclosure appears only under sustained traffic, once allocations are being recycled. The amount actually written also has to reach the consumer: sending the buffer's declared length rather than the number of bytes filled is the usual mechanism, and it is the same shape as CWE-170, where the consumer of a buffer reads past the bytes that were actually written into it.

Cryptographic state used before it is set

// VULNERABLE - crypto state never set
declare encryptor
encryptor.encrypt(data)  // key/IV never assigned - fails or uses a zero key

Why this is vulnerable: which of the two outcomes in that comment occurs is the problem. An implementation that throws on an unconfigured key is a bug caught immediately; one that proceeds with an all-zero key produces well-formed ciphertext with a key the attacker already knows, and the caller cannot tell the difference by looking at the output - it is the right length, it is not the plaintext, and it decrypts correctly in the round-trip test because the same zero key is used both ways.

That is the reason a round-trip assertion is not sufficient verification here. The test that distinguishes them is decrypting with an independently constructed key that was supplied explicitly, or asserting that the ciphertext of a known plaintext is not equal to a value produced under a zero key. Prefer APIs that take the key as a constructor argument, so an unconfigured object cannot be built at all.

Secure Patterns

// SECURE - explicit safe default, deny by default
declare isVerified = false
if check_signature():
    isVerified = true
if isVerified:
    process()

// SECURE - buffer zeroed before use
declare buffer[1024] = {0}
send_response(buffer)

// SECURE - the key and IV are checked before an encryptor can exist
key = load_key()
iv = generate_random_iv()
if key is unset or iv is unset:
    fail("key material not initialized")
declare encryptor = new Encryptor(key, iv)   // no unconfigured state to construct
encryptor.encrypt(data)

Why this works: Assigning a safe value at declaration removes the window where a variable's meaning depends on which code path executed first. Defaulting security flags to false means a skipped check fails closed instead of open. Zeroing buffers removes stale data as a source of information disclosure. Checking the key and IV before they reach the constructor turns a silent weak-crypto bug into a loud failure. Taking them as constructor arguments means there is no half-built encryptor for a later code path to reach: the check has to happen before the object exists, rather than being a step someone can forget to call afterwards.

Additional Resources