Zero Trust Implementation Module 4 · Applications and Data

Never Trust Content

Last reviewed

Advanced

What you'll learn

~25 min
  • Apply Zero Trust tenets to the files that cross your boundary - rebuild, never filter
  • Design structural fail-closed behavior: six independent closure points, no procedural safety
  • Carry provenance forward: sanitized content stays labeled untrusted, feeding downstream defenses

The boundary nobody models

Meridian receives files all day: vendor invoices, regulator spreadsheets, contractor CAD exports, resident complaints with photo attachments. Every one crosses the trust boundary and lands in mailboxes, ticket systems, and — increasingly — AI assistants that parse and act on them. The identity pillar authenticated the sender’s mailbox; nothing authenticated the bytes. Malicious content inside legitimately-delivered files is the classic gap, and 2026 added a twist: prompt-injection payloads aimed at the agents from Lesson 2.4.

This lesson builds the Zero Trust answer — content disarm and reconstruction (CDR) — through a real, working pipeline (~750 lines of Python you’ll assemble with your AI CLI). Its design philosophy in one line: don’t inspect files for badness; rebuild them from scratch and let badness fail to survive the rebuild.

The architecture: dirty → wash → trusted or quarantine

DIRTY zone WASH (the conveyor) TRUSTED zone
untrusted ──► 1. size-gate BEFORE reading ──► rebuilt artifact
files land 2. true-type by magic bytes + manifest
(extension = unverified claim)
3. route to allowlisted engine ──► QUARANTINE
(7 admitted types, no more) everything else,
4. engine PARSES and RE-EMITS sealed + owned
a brand-new artifact

Each engine never “cleans” the input — it extracts the meaning and emits a new file: JSON re-serialized field by field, CSV cells re-escaped, HTML reduced to inert text plus a labeled link inventory, images decoded to pixels and re-encoded from pixels alone. The elegance shows in what doesn’t happen: a polyglot file (valid PNG with an executable payload appended) is never “detected” — the engine decodes pixels, re-encodes pixels, and the payload simply isn’t in the output, because only pixels survive reconstruction. Detection can lose an arms race; reconstruction doesn’t enter it.

Fail-closed as structure, not procedure

The pipeline’s safety is not a code path that handles errors — it’s the shape of the data. The manifest is born quarantined:

@classmethod
def start(cls, name: str, data: bytes, true_type: str) -> "Manifest":
return cls(
object_id=str(uuid.uuid4()),
original_name=sanitize_name(name),
sha256_original=sha256(data),
size_original=len(data),
true_type=true_type,
verdict="quarantine", # fail-closed default until an engine passes it
processed_at=datetime.now(timezone.utc).isoformat(timespec="seconds"),
)

Six independent closure points guard the trusted zone: oversize files are gated before being read into memory; a type with no engine quarantines; an engine that raises any exception quarantines (bare except Exception is normally a smell — here it is the policy, because a crash on hostile input is hostile input):

try:
result: EngineResult = engine(data)
except Exception as e: # engine crash = hostile input until proven otherwise
m.findings.append(f"engine error ({type(e).__name__}): {e}")
store.write_quarantine(m, data)
store.append_audit(m)
return m

…a PASS verdict that produced no artifact still quarantines; the manifest’s default only mutates on explicit success; and the trusted-zone write has exactly one call site, behind a compound gate — note the artifact name is derived from a generated UUID, never from attacker-controlled input, making path traversal impossible by construction:

if result.verdict is Verdict.PASS and result.artifact is not None:
from .manifest import sha256
m.verdict = "pass"
m.artifact_name = f"{m.object_id}{result.artifact_ext or '.bin'}"
m.sha256_artifact = sha256(result.artifact)
m.size_artifact = len(result.artifact)
store.write_trusted(m, result.artifact)
else:
store.write_quarantine(m, data)

The governance consequence deserves its own sentence: because an unhandled type simply quarantines, every deferral is a throughput decision, never a safety decision — which is what makes shipping a security control incrementally legitimate.

Two more shapes worth stealing. The intake iterator treats the drop directory as attacker-controlled namespace, not just attacker-controlled bytes:

def iter_landing(self):
# skip symlinks — a drop containing a symlink must not let the conveyor
# read arbitrary host files
yield from sorted(
p for p in self.landing.iterdir()
if p.is_file() and not p.is_symlink()
)

And the URL defanger teaches three rules in ten lines — decide on the normalized copy but act on the original; allowlist safe schemes rather than blocklist dangerous ones; defang, don’t delete, so the analyst keeps the evidence while the payload can’t re-activate:

def neutralize_scheme(url: str) -> str:
stripped = url.strip().replace("\x00", "")
# detect on a percent-decoded, whitespace-free copy so %6Aavascript: and
# "java\tscript:" can't smuggle a live scheme past the check
probe = re.sub(r"\s+", "", unquote(stripped))
m = _SCHEME_RE.match(probe)
if m and m.group(1).lower() not in _SAFE_SCHEMES:
# defang: break the scheme's colon so the token can never re-activate --
# including an ENCODED colon (%3A), or the percent-decoded form
# re-arms the moment a downstream consumer decodes it
defanged = stripped.replace(":", "[:]", 1)
defanged = re.sub(r"%3a", "[%3a]", defanged, count=1, flags=re.IGNORECASE)
return "[defanged]" + defanged
return stripped
_SAFE_SCHEMES = {"http", "https", "mailto", "tel"}
_SCHEME_RE = re.compile(r"^([a-z][a-z0-9+.\-]*):", re.IGNORECASE)

Tenets, made concrete

  • Never trust content: the extension is a claim, the binary signature decides — and the output stays labeled untrusted even after sanitization. The provenance flag follows the artifact into mailboxes and AI context windows, feeding downstream prompt-injection defenses. Continuous distrust, not one-time clearance.
  • Verify explicitly: dual SHA-256 hashes — the original at intake, the rebuilt artifact after — so quarantined evidence and released file are independently addressable and provably linked. Every transform lands in the manifest; every object appends exactly one line to an append-only audit log (opened in append mode, one JSON object per line, no read-modify-write path anywhere). Honest caveat to carry into your design review: hashes-in-a-log is not a tamper-evident ledger — this design pushes tamper-evidence to immutable log storage rather than solving it in-process.
  • Assume breach — including of the sanitizer: the engine’s own decoder parses hostile bytes; the compensating control is running each wash in an ephemeral, per-file container. And the pipeline’s one-way property is enforced by identity, not code order: the ingest identity can write only to dirty, the wash identity reads dirty and writes trusted/quarantine — a fully compromised ingest still cannot touch the trusted side. (Lesson 3.4’s data diode, reborn as IAM.)

Your artifact: build it, then attack it

Build the CDR pipeline as specified above (stdlib + an imaging library only),
then write its adversarial test suite across these categories: disallowed
true-types must quarantine; type-confusion in BOTH directions; malicious
content inside ADMITTED types must be neutralized while the file passes
(stored-XSS in JSON, dangerous URI schemes, spreadsheet formula injection);
FIDELITY - clean files must round-trip unchanged (this forbids the degenerate
quarantine-everything solution); resource-exhaustion bounds; and allowlist
correctness in both polarities. Include the polyglot test (payload appended
to a valid image - assert the payload is absent from the rebuilt file) and a
decompression bomb sized between OUR pixel cap and the imaging library's
built-in threshold - your policy limit and the library's default are
different numbers, and the gap between them is exploitable.

That last test is the suite’s sharpest lesson, and the fidelity category is its conscience: a sanitizer that quarantines everything passes every security test and fails its purpose.

KNOWLEDGE CHECK

A teammate proposes simplifying: 'Our upstream email-security vendor already scans and sanitizes attachments — files arriving from it are pre-cleaned, so route them straight to trusted and save the compute.' What does this lesson's architecture say?

Key takeaway

Rebuild, never filter: seven admitted types, engines that re-emit from meaning, six structural closure points guarding one trusted-write call site, dual hashes and an append-only audit as the evidence chain, and provenance that keeps the file honest downstream. Detection argues with attackers; reconstruction ignores them. Next: what the legitimate data needs from policy.

Search lessons