Skip to content

feat:implement APTS-MR-024: Final LLM Output Sanitization and Validation - #72

Open
rashim27us wants to merge 3 commits into
OWASP:mainfrom
rashim27us:mr-a04-final-output-sanitization
Open

feat:implement APTS-MR-024: Final LLM Output Sanitization and Validation#72
rashim27us wants to merge 3 commits into
OWASP:mainfrom
rashim27us:mr-a04-final-output-sanitization

Conversation

@rashim27us

Copy link
Copy Markdown
Contributor

Summary

Closes #71

This PR adds advisory requirement APTS-MR-A04: Final LLM Output Sanitization and Downstream Context Isolation (Advisory) to the standard, addressing the risk of untrusted content reflection and prompt-injection-driven payload emission through LLM outputs (OWASP Top 10 for LLM Applications 2025: LLM05:2025 Improper Output Handling).

Following the advisory-first path agreed upon in #71, this practice is introduced as an Advisory Requirement in v0.1.0 to enable immediate adoption, laying the groundwork for promotion to normative APTS-MR-024 (MUST | Tier 2) in v0.2.0.


Key Changes & Refinements

  1. New Advisory Requirement (standard/appendix/Advisory_Requirements.md):

    • Context-Aware Output Encoding: Enforces contextual encoding (HTML entity encoding, JSON escaping) for report artifacts, customer exports, and web UI dashboards to prevent Stored XSS.
    • PoC Execution-Boundary Validation: Explicitly requires validation at the final execution dispatch boundary using structured/parameterized argument vectors (argv vectors, shell=False; prohibiting string concatenation to sh -c or system()).
    • Egress & Webhook Sanitization: Validates URLs and webhook destinations against SSRF, cloud metadata endpoints (169.254.169.254), and loopback/RFC 1918 addresses.
    • Internal Sink Isolation: Treats internal platform sinks (finding scoring engines, evidence stores, structured audit logs) as distinct consumption targets to prevent internal log injection or metric manipulation.
    • Presentation Layer Isolation: Requires sandboxed UI rendering (e.g., iframe sandboxing, strict CSP).
    • Cross-Reference: Adds a pointer distinguishing finding authenticity/fabrication verification (APTS-RP-A01) from payload injection containment (APTS-MR-A04).
    • Test Corpus & Verification: Includes concrete test cases for Stored XSS, PoC command injection, integration SSRF/metadata poisoning, and internal sink poisoning.
  2. Implementation Guide (standard/6_Manipulation_Resistance/Implementation_Guide.md):

    • Added implementation section with a Pydantic schema validation pattern for data deliverables and a parameterized subprocess dispatch function for executable PoC code.
    • Documented key architectural considerations and common pitfalls.
  3. Manipulation Resistance Domain README (standard/6_Manipulation_Resistance/README.md):

    • Updated the domain advisory count from three to four.
    • Added a "See also" reference for APTS-MR-A04 noting its candidate promotion to APTS-MR-024 in v0.2.0.
  4. Advisory Count Synchronization (19 → 20):
    synchronized the global advisory count across all affected prose documentation files:

    • README.md (Nineteen → Twenty)
    • index.md (Nineteen → Twenty)
    • standard/README.md (19 → 20)
    • standard/Introduction.md (19 → 20)
    • standard/Frontispiece.md (19 → 20)
    • standard/Getting_Started.md (19 → 20)
    • standard/appendix/Glossary.md (19 → 20)
    • standard/appendix/Vendor_Evaluation_Guide.md (19 → 20)
  5. Normative Baseline:

    • Preserves the 173 normative requirements count across three compliance tiers (Tier 1: 72, Tier 2: 157, Tier 3: 173).
    • standard/apts_requirements.json remains untouched.

Verification

  • Verified that all 20 advisories in Advisory_Requirements.md parse with correct numbering and headers.
  • Verified that all prose references across the documentation reflect exactly 20 advisory practices with zero stale references remaining.
  • Verified cross-references to APTS-MR-002, APTS-MR-018, APTS-MR-022, APTS-SC-020, APTS-RP-001, and APTS-RP-A01.

@jinsonvarghese

Copy link
Copy Markdown
Member

@ossumpossum looping you in, in case you wanted to review this. :)

@jinsonvarghese

Copy link
Copy Markdown
Member

@rashim27us in the meantime, two spots in MR-A04's own body use capitalized MUST/SHOULD (the Practice Description intro, and item 2). Every other advisory uses lowercase modals or plain imperative phrasing in its own text, since RFC 2119 casing is reserved for actual normative requirements. Could you lowercase both?

@ossumpossum

Copy link
Copy Markdown
Contributor

Went a bit past just the dispatch-boundary wording since I was in the file. Flag if you'd rather I split the SSRF stuff into a separate issue.

Double-checked against primary sources / the actual repo:

  • LLM05:2025 Improper Output Handling: confirmed against OWASP's own site, and the earlier in-thread fix from the 2023 numbering is applied correctly everywhere it appears. One heads-up though: OWASP shipped a 2026 edition four days ago that already renumbers this. LLM10:2026 is now Improper Output Handling, and LLM05:2026 is Data and Model Poisoning. Worth citing LLM10:2026 instead so this doesn't read stale by the time it's normative in v0.2.0.
  • Advisory count 19→20 checks out; none of the tier-required counts (72/85/16/173) were touched, correct since this is advisory-only.
  • All 5 cross-referenced requirement IDs (MR-002, MR-018, MR-022, SC-020, RP-001) are real and topically accurate. MR-002 is inbound target-data sanitization, MR-022 is specifically inter-model pipeline boundaries, not final external output, so MR-A04 is a genuinely distinct gap rather than overlap.
  • RP-A01 cross-reference is accurate, both anchor links resolve, and the subprocess dispatch pattern (array argv, shell=False, binary allowlist) is sound.

One real gap in validate_egress_url, worth tightening before this reaches implementers:

Tested it against a few known SSRF bypass patterns. Even as illustrative example code rather than a hardened reference impl, this is going to get copied verbatim, so worth closing:

blocked   : http://10.0.0.5/
blocked   : http://192.168.1.5/
ALLOWED   : http://172.16.0.5/         172.16.0.0/12 is RFC 1918 too, currently unchecked
ALLOWED   : http://172.31.255.254/
ALLOWED   : http://169.254.170.2/      AWS ECS task-metadata / creds endpoint
ALLOWED   : http://[::1]/              IPv6 loopback
ALLOWED   : http://0x7f000001/         many HTTP/socket libs treat this as 127.0.0.1
                                        (inet_aton-style numeric parsing, not DNS)
ALLOWED   : http://2130706433/         same, decimal-encoded 127.0.0.1

Root cause is that it's a hostname string match with no resolution behind it, so beyond the specific ranges above, it's also open to plain DNS rebinding: evil.com resolving to 169.254.169.254 matches none of the listed prefixes.

Something like this closes the cases above (tested against all of them, plus a couple of ordinary external domains to make sure nothing legitimate gets blocked):

import ipaddress, socket

def validate_egress_url(cls, v: str) -> str:
    parsed = urlparse(v)
    if parsed.scheme not in {"http", "https"}:
        raise ValueError(f"Disallowed URL scheme: {parsed.scheme}")
    hostname = parsed.hostname or ""
    try:
        ip = ipaddress.ip_address(hostname)
    except ValueError:
        try:
            ip = ipaddress.ip_address(socket.gethostbyname(hostname))
        except socket.gaierror:
            raise ValueError("Could not resolve egress destination")  # fail closed
    if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved or ip.is_multicast:
        raise ValueError("Forbidden internal or metadata egress destination")
    return v

Two caveats on this fix, not defects in the original so much as things worth being upfront about:

  • socket.gethostbyname() is a blocking call: fine in a sync context, worth a nonblocking resolver if this validator ever runs on an async path.
  • Resolving at validation time still isn't the full fix. OWASP's own SSRF cheat sheet calls this out directly: validating a hostname doesn't guarantee the same IP is used when the request actually fires, since "a DNS resolution will be made when the business code will be executed." Closing that fully means pinning the resolved IP through to the actual outbound connection, not just at the Pydantic layer.

Smaller stuff:

  • import shlex isn't used anymore now that dispatch is array-based, probably a leftover from an earlier draft.
  • Alibaba Cloud's metadata endpoint is 100.100.100.200, not 169.254.169.254, worth a footnote if "cloud metadata endpoints" is meant to cover more than AWS/Azure/GCP.
  • The iframe sandbox="allow-same-origin" as written is fine on its own, but worth an explicit line warning against ever pairing it with allow-scripts. That combination is a known sandbox-escape pattern, and whoever adds interactivity to a dashboard later is exactly who'd reach for both.

Dispatch-boundary design and the rest of this holds up well. The egress check is the one thing I'd want closed before this merges, everything else here is take-it-or-leave-it.

@rashim27us

Copy link
Copy Markdown
Contributor Author

Thank @jinsonvarghese @ossumpossum for the sharp review! All applied and pushed:

  • OWASP Top 10 citation: Updated to cite LLM10:2026 Improper Output Handling (noting its prior LLM05:2025 designation) across the advisory and domain README.
  • Hardened SSRF validator: Updated validate_egress_url to resolve hostnames via ipaddress + socket.gethostbyname(), closing the RFC 1918 172.16.0.0/12, link-local/ECS metadata (169.254.170.2), IPv6 loopback ([::1]), and hex/decimal IP bypasses. Also explicitly blocked Alibaba Cloud's metadata IP (100.100.100.200).
  • Implementation notes: Added explicit callouts for TOCTOU / IP pinning (referencing OWASP's SSRF Prevention Cheat Sheet), async DNS considerations for gethostbyname(), and removed the unused shlex import.
  • Sandbox escape warning: Added an explicit warning never to pair sandbox="allow-same-origin" with allow-scripts on untrusted frames.
  • Modal casing: Lowercased the SHOULD/MUST instances in MR-A04's body.

@jinsonvarghese

Copy link
Copy Markdown
Member

Thank you @rashim27us.

@ossumpossum will wait for a day or two to see if you have anything to add after a final review.

@ossumpossum

Copy link
Copy Markdown
Contributor

Final review done. Pulled the current diff and re-ran the same bypass list against the updated validator rather than going off the changelog: 172.16.0.5, 172.31.255.254, 169.254.170.2, [::1], and both encoded-IP forms all block now, example.com still resolves fine, and the Alibaba IP is explicitly listed too. shlex is gone, the sandbox-escape warning is in, the modal casing in the Practice Description intro and item 2 is lowercase, and the LLM10:2026 citation swap checks out.

Nothing further from me, good to merge as far as I'm concerned.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feat: Add Requirement for Final LLM Output Sanitization / Validation

3 participants