Sections in this lesson
Secure Output Handling
A model can return valid JSON containing an unauthorized resource, a dangerous URL or invented SQL.
Reviewed: · Content owner: Interview Preparator
What you will learn
- Validate syntax and domain separately
- Render text as text
- Keep the negative contract visible
Before you start
The flow
- 1.Identify scope
- 2.Inspect evidence
- 3.Apply host control
- 4.Run negative cases
- 5.Compare benign behavior
- 6.Review remaining risks
Validate syntax and domain separately
A model can return valid JSON containing an unauthorized resource, a dangerous URL or invented SQL. Parse a narrow schema, reject extra fields and then validate business constraints and authorization. Never treat syntactic validity as permission.
Use parameterized database operations and fixed tool implementations. Passing model text into a shell, SQL concatenation or a generic executor changes data into code and expands the attack surface.
Render text as text
escapedText encodes HTML-sensitive characters for a plain-text HTML context. In React, ordinary string interpolation already escapes text. Do not use raw HTML insertion for generated content or treat this helper as a sanitizer for every output context.
URLs, attributes, CSS and JavaScript have different rules. A safe text encoder does not validate a link target, and a valid link does not prove that a remote image or redirect is safe.
Keep the negative contract visible
The fixtures include script, image, iframe, SVG and javascript-link strings. Tests verify encoded output rather than executing them. Add legitimate code samples so defensive rendering does not erase useful generic type syntax.
Existing question content is repository-authored and follows its own formatting helper. Do not reuse that trusted-content path for model output. Apply the correct sink-specific control at every boundary.
Try it
import { createHash, timingSafeEqual } from 'node:crypto';export function payloadDigest(call) { // Canonical ordered fields; extra or ambiguous fields cannot bypass payload binding. if (!call || Object.keys(call).sort().join(',') !== 'action,resource' || !['readPlan','completePlan'].includes(call.action) || !/^PLAN-[0-9]+$/.test(call.resource)) throw new Error('Invalid call'); return createHash('sha256').update(JSON.stringify([call.action,call.resource])).digest('hex');}export function authorize(scope, call, resource, grant, now, epoch = 1) { const digest = payloadDigest(call); if (!Number.isFinite(now) || !Number.isSafeInteger(epoch)) return 'DENY'; if (!scope || !resource || resource.id !== call.resource || resource.tenant !== scope.tenant || !resource.readers.includes(scope.user)) return 'DENY'; if (call.action === 'readPlan') return 'ALLOW'; // Grant comes from a trusted server-side approval store, never from model output. if (!grant || !Number.isFinite(grant.expiresAt) || grant.revoked || grant.epoch !== epoch || grant.expiresAt <= now || grant.tenant !== scope.tenant || grant.user !== scope.user || grant.runId !== scope.runId || grant.digest !== digest) return 'APPROVAL_REQUIRED'; return 'ALLOW';}export function escapedText(value) { if (typeof value !== 'string' || value.length > 4000) throw new Error('Invalid output text'); return value.replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('"','"').replaceAll("'",''');}export function allowedUrl(value, hosts) { try { const url = new URL(value); return url.protocol === 'https:' && !url.username && !url.password && url.port === '' && hosts.includes(url.hostname); } catch { return false; }}export function trustedToken(claims, policy, now) { // Claims must already be cryptographically verified by an auth adapter. return claims?.issuer === policy.issuer && claims?.audience === policy.audience && claims?.expiresAt > now && claims?.tenant === policy.tenant && Array.isArray(claims?.scopes) && claims.scopes.includes(policy.scope);}export function approvedArtifact(bytes, expectedSha256) { const actual = createHash('sha256').update(bytes).digest(); if (!/^[a-f0-9]{64}$/.test(expectedSha256 ?? '')) return false; return timingSafeEqual(actual,Buffer.from(expectedSha256,'hex'));}Run npm run examples:security and npm run eval:security. This source is a deterministic teaching fixture; it does not prove live model safety, NLI, production auth or OS/network isolation.
Interview questions
Does valid JSON authorize execution?
Use parameterized database operations and fixed tool implementations. Passing model text into a shell, SQL concatenation or a generic executor changes data into code and expands the attack surface.
Is HTML escaping safe in every output context?
URLs, attributes, CSS and JavaScript have different rules. A safe text encoder does not validate a link target, and a valid link does not prove that a remote image or redirect is safe.
May model output use the trusted repository HTML path?
Existing question content is repository-authored and follows its own formatting helper. Do not reuse that trusted-content path for model output. Apply the correct sink-specific control at every boundary.
Practice
Extend one negative scenario and its authorized positive counterexample for Secure Output Handling.
Check your solution
- State the trusted input and effect boundary.
- Assert the expected status and observed effect.
- Report coverage and limitations without claiming production readiness.
Official references
Continue learning
Category sequence
AI Security
Lesson 4 / 14
Was this lesson useful?
Change notes: 2026-10-31: security/reliability curriculum and source-bound fixture.