Incomplete Cleanup
Description
Incomplete Cleanup is a vulnerability where the product does not properly clean up and remove temporary or supporting resources after they have been used. When applications create temporary files, allocate memory, open network connections, or acquire other resources, they must properly release these resources when they are no longer needed. Failure to do so can lead to resource exhaustion, information disclosure through leftover data, and system instability. This is particularly critical for temporary files that may contain sensitive information.
Risk
Incomplete cleanup creates multiple security risks. Temporary files containing sensitive data like database credentials, session tokens, or encryption keys may persist on disk and be accessed by attackers. Resource exhaustion occurs when applications continuously allocate resources without releasing them, eventually causing denial of service. Directories have limits on file counts, and overflow can cause system-wide failures. In multi-tenant environments, residual data from one user's session may be accessible to subsequent users. NTFS alternate data streams can retain information even after apparent file deletion.
Solution
Delete or release temporary files and other supporting resources immediately after they are no longer needed. Use try-finally blocks or equivalent constructs to ensure cleanup occurs even when exceptions are thrown. Create temporary files with restrictive permissions and in secure directories. Consider using memory-mapped files or in-memory storage for highly sensitive temporary data that should never touch disk. Implement periodic cleanup routines to catch resources that escape normal cleanup paths. Use secure deletion methods for sensitive data that zero out content before deletion.
Common Consequences
| Impact | Details |
|---|---|
| Availability | Scope: Availability DoS: Resource Consumption - Temporary files or resources accumulate, potentially exhausting disk space, file handles, or memory. |
| Confidentiality | Scope: Confidentiality Read Application Data - Sensitive information in temporary files may be exposed to unauthorized actors. |
| Integrity | Scope: Integrity Unexpected State - Leftover resources may cause unexpected application state or behavior on subsequent runs. |
Example Code
Vulnerable Code
// Vulnerable: Stream not closed on exception
public class VulnerableFileReader {
public byte[] readFile(String path) {
try {
InputStream is = new FileInputStream(path);
byte[] data = new byte[is.available()];
is.read(data);
is.close(); // Vulnerable: Not called if read() throws
return data;
} catch (IOException e) {
log.error("Read failed: " + e.getMessage());
return null;
// Vulnerable: Stream left open on exception
}
}
// Vulnerable: Temporary file not deleted
public void processLargeData(byte[] data) throws IOException {
File tempFile = File.createTempFile("process_", ".tmp");
FileOutputStream fos = new FileOutputStream(tempFile);
fos.write(data);
fos.close();
processFile(tempFile);
// Vulnerable: tempFile not deleted
// Vulnerable: What if processFile() throws?
}
}
# Vulnerable: Resources not cleaned up
import tempfile
import os
class VulnerableProcessor:
def process_data(self, data):
# Vulnerable: Temp file may not be deleted
temp_fd, temp_path = tempfile.mkstemp(suffix='.dat')
try:
os.write(temp_fd, data)
os.close(temp_fd)
result = self.analyze_file(temp_path)
# Vulnerable: If analyze_file raises, file is never deleted
os.unlink(temp_path)
return result
except Exception as e:
# Vulnerable: temp_path still exists on disk
raise
def create_session(self, user_id):
# Vulnerable: Session data persists indefinitely
session = {
'user_id': user_id,
'token': generate_token(),
'created': time.time()
}
# Vulnerable: No cleanup mechanism
# Sessions accumulate forever
self.sessions[session['token']] = session
return session['token']
// Vulnerable: Memory and file handle leaks
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char* vulnerable_process_file(const char* filename) {
FILE* file = fopen(filename, "r");
if (!file) {
return NULL;
}
fseek(file, 0, SEEK_END);
long size = ftell(file);
fseek(file, 0, SEEK_SET);
char* buffer = malloc(size + 1);
if (!buffer) {
// Vulnerable: file handle leaked
return NULL;
}
if (fread(buffer, 1, size, file) != size) {
// Vulnerable: buffer leaked, file handle leaked
return NULL;
}
buffer[size] = '\0';
fclose(file);
// Process and return subset
char* result = extract_data(buffer);
// Vulnerable: original buffer never freed
return result;
}
// Vulnerable: Sensitive data left in memory
void process_password(const char* password) {
char local_copy[256];
strncpy(local_copy, password, sizeof(local_copy) - 1);
hash_and_store(local_copy);
// Vulnerable: password remains in local_copy
// Stack memory not zeroed, could be recovered
}
// Vulnerable: Database connections not cleaned up
public class VulnerableDatabase {
private List<SqlConnection> openConnections = new List<SqlConnection>();
public DataTable ExecuteQuery(string query) {
SqlConnection conn = new SqlConnection(connectionString);
conn.Open();
// Vulnerable: Connection added but never removed
openConnections.Add(conn);
SqlCommand cmd = new SqlCommand(query, conn);
SqlDataAdapter adapter = new SqlDataAdapter(cmd);
DataTable results = new DataTable();
adapter.Fill(results);
// Vulnerable: Connection not closed
// Vulnerable: Command and adapter not disposed
return results;
}
// Vulnerable: Temporary table not cleaned up
public void ProcessBatch(List<Record> records) {
using (var conn = new SqlConnection(connectionString)) {
conn.Open();
// Create temp table
ExecuteNonQuery(conn, "CREATE TABLE #TempBatch (Id INT, Data NVARCHAR(MAX))");
foreach (var record in records) {
InsertIntoTemp(conn, record);
}
ProcessTempTable(conn);
// Vulnerable: Temp table not dropped
// Session ends but table may persist in tempdb
}
}
}
Fixed Code
// Fixed: Proper resource cleanup with try-with-resources
public class SecureFileReader {
public byte[] readFile(String path) {
// Fixed: try-with-resources ensures closure
try (InputStream is = new FileInputStream(path)) {
byte[] data = new byte[is.available()];
is.read(data);
return data;
} catch (IOException e) {
log.error("Read failed: " + e.getMessage());
return null;
}
// Stream automatically closed even on exception
}
public void processLargeData(byte[] data) throws IOException {
// Fixed: Temp file with automatic deletion
Path tempFile = Files.createTempFile("process_", ".tmp");
try {
Files.write(tempFile, data);
processFile(tempFile.toFile());
} finally {
// Fixed: Always delete temp file
try {
// Fixed: Secure deletion - overwrite before delete
secureDelete(tempFile);
} catch (IOException e) {
log.warn("Failed to delete temp file: " + tempFile);
}
}
}
private void secureDelete(Path file) throws IOException {
// Fixed: Overwrite with zeros before deletion
long size = Files.size(file);
try (FileChannel channel = FileChannel.open(file, StandardOpenOption.WRITE)) {
ByteBuffer zeros = ByteBuffer.allocate(8192);
long remaining = size;
while (remaining > 0) {
zeros.clear();
int toWrite = (int) Math.min(remaining, zeros.capacity());
zeros.limit(toWrite);
channel.write(zeros);
remaining -= toWrite;
}
}
Files.delete(file);
}
}
# Fixed: Proper resource cleanup
import tempfile
import os
import contextlib
import time
class SecureProcessor:
def process_data(self, data):
# Fixed: Use context manager for temp file
with tempfile.NamedTemporaryFile(suffix='.dat', delete=False) as temp:
temp_path = temp.name
temp.write(data)
try:
result = self.analyze_file(temp_path)
return result
finally:
# Fixed: Always clean up
self._secure_delete(temp_path)
def _secure_delete(self, path):
"""Securely delete file by overwriting then removing."""
try:
# Fixed: Overwrite with random data
file_size = os.path.getsize(path)
with open(path, 'wb') as f:
f.write(os.urandom(file_size))
os.unlink(path)
except OSError as e:
logger.warning(f"Failed to secure delete {path}: {e}")
def create_session(self, user_id, timeout_seconds=3600):
session = {
'user_id': user_id,
'token': generate_token(),
'created': time.time(),
'expires': time.time() + timeout_seconds
}
self.sessions[session['token']] = session
# Fixed: Schedule cleanup
self._schedule_session_cleanup(session['token'], timeout_seconds)
return session['token']
def cleanup_expired_sessions(self):
"""Fixed: Periodic cleanup of expired sessions."""
now = time.time()
expired = [
token for token, session in self.sessions.items()
if session['expires'] < now
]
for token in expired:
del self.sessions[token]
logger.info(f"Cleaned up expired session: {token[:8]}...")
// Fixed: Proper memory and file handle cleanup
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char* data;
int error;
char error_msg[256];
} FileResult;
FileResult secure_process_file(const char* filename) {
FileResult result = {NULL, 0, ""};
FILE* file = NULL;
char* buffer = NULL;
file = fopen(filename, "r");
if (!file) {
result.error = 1;
snprintf(result.error_msg, sizeof(result.error_msg),
"Cannot open file");
goto cleanup;
}
fseek(file, 0, SEEK_END);
long size = ftell(file);
fseek(file, 0, SEEK_SET);
buffer = malloc(size + 1);
if (!buffer) {
result.error = 2;
snprintf(result.error_msg, sizeof(result.error_msg),
"Memory allocation failed");
goto cleanup; // Fixed: File will be closed
}
if (fread(buffer, 1, size, file) != size) {
result.error = 3;
snprintf(result.error_msg, sizeof(result.error_msg),
"Read failed");
goto cleanup; // Fixed: Both cleaned up
}
buffer[size] = '\0';
result.data = extract_data(buffer);
cleanup:
// Fixed: Always cleanup resources
if (buffer) {
// Fixed: Zero before freeing
memset(buffer, 0, size);
free(buffer);
}
if (file) {
fclose(file);
}
return result;
}
// Fixed: Sensitive data zeroed after use
void process_password(const char* password) {
char local_copy[256];
strncpy(local_copy, password, sizeof(local_copy) - 1);
local_copy[sizeof(local_copy) - 1] = '\0';
hash_and_store(local_copy);
// Fixed: Zero out password in memory
explicit_bzero(local_copy, sizeof(local_copy));
}
// Fixed: Proper database connection cleanup
public class SecureDatabase : IDisposable {
private bool disposed = false;
public DataTable ExecuteQuery(string query) {
// Fixed: Using statement ensures disposal
using (SqlConnection conn = new SqlConnection(connectionString))
using (SqlCommand cmd = new SqlCommand(query, conn))
using (SqlDataAdapter adapter = new SqlDataAdapter(cmd)) {
conn.Open();
DataTable results = new DataTable();
adapter.Fill(results);
return results;
}
// All resources automatically cleaned up
}
public void ProcessBatch(List<Record> records) {
using (var conn = new SqlConnection(connectionString)) {
conn.Open();
try {
ExecuteNonQuery(conn, "CREATE TABLE #TempBatch (Id INT, Data NVARCHAR(MAX))");
foreach (var record in records) {
InsertIntoTemp(conn, record);
}
ProcessTempTable(conn);
}
finally {
// Fixed: Always drop temp table
try {
ExecuteNonQuery(conn, "DROP TABLE IF EXISTS #TempBatch");
}
catch (SqlException) {
// Temp table may already be dropped or never created
}
}
}
}
public void Dispose() {
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing) {
if (!disposed) {
if (disposing) {
// Fixed: Clean up managed resources
CleanupAllConnections();
}
disposed = true;
}
}
}
CVE Examples
- CVE-2000-0552 - World-readable temporary file persisted after use, exposing sensitive data.
- CVE-2005-2293 - Undeleted temporary file leaked database credentials.
- CVE-2002-2066 - NTFS alternate data streams retained data after file wiping.
- CVE-2002-2067 - File wiping utility failed to remove NTFS alternate data streams.
- CVE-2002-2068 - Incomplete cleanup of NTFS alternate data streams.
- CVE-2002-2069 - Residual data in NTFS streams after deletion.
- CVE-2002-2070 - Data remnants in NTFS alternate streams persisted.
References
- MITRE Corporation. "CWE-459: Incomplete Cleanup." https://cwe.mitre.org/data/definitions/459.html
- CERT C Secure Coding Standard. "MEM00-C. Allocate and free memory in the same module, at the same level of abstraction."