CMMC 2.0 MP.L1-3.8.3 (NIST 800-171r2 3.8.3) requires that FCI is sanitized or destroyed before media is disposed of or reused. In software systems, this translates to hard deletion of user data on account closure, secure cleanup of temporary files after processing, and server-side session invalidation on logout. Soft-delete patterns that only set deleted_at leave FCI in the database indefinitely — accessible to database administrators, backup restores, and potential future bugs that bypass the soft-delete filter. CWE-459 (Incomplete Cleanup) applies when temporary file handles or session records outlive their authorized lifecycle.
High because retained FCI after account deletion or logout constitutes a CMMC data persistence violation that survives the user's explicit revocation of consent.
Implement hard delete in a transaction that cascades across all user-owned records. Use try/finally to guarantee temporary file cleanup even when processing throws:
// app/api/users/[id]/route.ts
export async function DELETE(req: Request, { params }: { params: { id: string } }) {
const session = await getServerSession()
if (!session || session.user.id !== params.id) {
return Response.json({ error: 'Forbidden' }, { status: 403 })
}
await db.$transaction([
db.document.deleteMany({ where: { userId: params.id } }),
db.session.deleteMany({ where: { userId: params.id } }),
db.user.delete({ where: { id: params.id } }),
])
const res = Response.json({ ok: true })
res.headers.set('Set-Cookie', 'session=; HttpOnly; Max-Age=0; Path=/')
return res
}
For upload processing in lib/file-processing.ts, write to a temp path and clean up in a finally block — the file is removed whether the processing succeeds or fails. Soft-delete patterns without a scheduled purge job do not satisfy MP.L1-3.8.3.
ID: gov-cmmc-level-1.media-protection.data-sanitization
Severity: high
CMMC Practice: MP.L1-3.8.3
What to look for: Look for data deletion handlers, account cleanup code, temporary file management, cache clearing, and database record purging logic. Check for hard delete vs soft delete patterns — soft deletes (setting deleted_at timestamps) retain data and are not sufficient for sanitization unless accompanied by a purge mechanism. Look at account deletion flows: do they delete the user's records, or just mark the account inactive? Check for temporary file upload handling — are uploaded files cleaned up after processing? Examine session invalidation on logout — is session data actually removed from the server?
Pass criteria: Count all data deletion pathways in the codebase. Hard delete capability exists for sensitive data. Account deletion removes or anonymizes at least 90% of user data rather than just deactivating. Temporary files are cleaned up after processing. Report: "X data deletion pathways found covering Y data types."
Fail criteria: No data deletion functionality exists for user data. Soft-delete-only with no purge path. Temporary files persist indefinitely. Session data remains in the database after logout (only client cookie cleared). File uploads never cleaned up.
Skip (N/A) when: Application does not store or process FCI or any sensitive user data (fully public, no user accounts, no data retention).
Detail on fail: Specify the sanitization gap. Example: "Account deletion in DELETE /api/users/[id] sets active=false but does not delete user records, documents, or uploaded files. No purge job exists. Temp files in /tmp are never cleaned." Keep under 500 characters.
Remediation: Implement proper data deletion on account closure and add cleanup for temporary data:
// app/api/users/[id]/route.ts
export async function DELETE(req: Request, { params }: { params: { id: string } }) {
const session = await getServerSession()
if (!session || session.user.id !== params.id) {
return Response.json({ error: 'Forbidden' }, { status: 403 })
}
// Hard delete in a transaction — cascade removes all related records
await db.$transaction([
db.document.deleteMany({ where: { userId: params.id } }),
db.session.deleteMany({ where: { userId: params.id } }),
db.user.delete({ where: { id: params.id } })
])
// Invalidate current session cookie
const response = Response.json({ ok: true })
response.headers.set('Set-Cookie', 'session=; HttpOnly; Max-Age=0; Path=/')
return response
}
For temporary files, use try/finally to ensure cleanup even on error:
// lib/file-processing.ts
import fs from 'fs/promises'
import path from 'path'
import os from 'os'
export async function processUpload(buffer: Buffer): Promise<string> {
const tmpFile = path.join(os.tmpdir(), `upload-${Date.now()}`)
await fs.writeFile(tmpFile, buffer)
try {
const result = await analyzeFile(tmpFile)
return result
} finally {
// Always clean up — even if processing throws
await fs.unlink(tmpFile).catch(() => {})
}
}