Skip to content

fix(template): inline tag fail-closed sandbox and case-insensitive extension tests (#931, #932)#935

Merged
zeroedin merged 5 commits into
mainfrom
feature/inline-tag-hardening
Jul 11, 2026
Merged

fix(template): inline tag fail-closed sandbox and case-insensitive extension tests (#931, #932)#935
zeroedin merged 5 commits into
mainfrom
feature/inline-tag-hardening

Conversation

@zeroedin

Copy link
Copy Markdown
Owner

Summary

  • Issue inline tag: fail closed when _contentRoot is empty #932: Flip the if contentRoot != "" guard in the inline tag's path-traversal sandbox to a hard error when _contentRoot is missing from the render context. Previously the sandbox was silently skipped; now the tag fails closed.
  • Issue inline tag: add mixed-case extension test #931: No code changes needed — the existing strings.ToLower on the extension already handles case-insensitive matching. The new spec tests (.SVG accepted, .PNG rejected) confirm this behavior and prevent regression if ToLower were removed.

Implementation

One-line logic inversion in internal/template/inline.go:

// Before: silently skip sandbox
if contentRoot != "" { ... }

// After: fail closed
if contentRoot == "" {
    return "", fmt.Errorf("inline tag requires _contentRoot in render context")
}

Test results

All 333 template specs pass. Full suite green with -race.

333 Passed | 0 Failed | 0 Pending | 0 Skipped

Closes #931, closes #932

🤖 Generated with Claude Code

zeroedin and others added 4 commits July 11, 2026 01:28
Issue #932 (fail closed): Add test verifying inline tag returns an error
when _contentRoot is missing from the render context, rather than
silently skipping the path-traversal sandbox. Update existing tests 1,
2, and 5 to include _contentRoot to match production context shape.

Issue #931 (mixed-case extension): Add two regression tests verifying
case-insensitive extension matching — .SVG is accepted (text allowlist)
and .PNG is rejected (binary blocklist).

PLAN.md and IMPLEMENTATION.md updated with fail-closed requirement and
case-insensitive extension note.

Test status:
- returns error when _contentRoot is not set (#932): RED
- matches file extensions case-insensitively (#931): GREEN
- rejects binary file type with mixed-case extension (#931): GREEN

Refs #931, #932

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix allowlist/blocklist terminology in all three files — the inline tag
code enforces a binary-extension blocklist, not an allowlist. The
PLAN.md "allowed file types" list is informational only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…context (#931, #932)

The path-traversal sandbox in the inline tag was behind an
`if contentRoot != ""` guard, silently skipping the check when
`_contentRoot` was absent from the render context. Flip the guard
to return an error instead, matching the existing `_contentDir` check.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@netlify

netlify Bot commented Jul 11, 2026

Copy link
Copy Markdown

Deploy Preview for alloyssg ready!

Name Link
🔨 Latest commit bb2ce4b
🔍 Latest deploy log https://app.netlify.com/projects/alloyssg/deploys/6a51db351b2da200085febef
😎 Deploy Preview https://deploy-preview-935--alloyssg.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the {% inline %} tag’s path traversal sandbox by failing closed when _contentRoot is missing from the render context, and adds spec tests to lock in case-insensitive extension handling.

Changes:

  • Make {% inline %} return an error if _contentRoot is missing/empty (fail-closed sandbox).
  • Add spec tests verifying case-insensitive extension handling (.SVG accepted, .PNG rejected).
  • Update plan/implementation docs to reflect the new fail-closed behavior and document case-insensitive extension normalization.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
plans/PLAN.md Updates inline-tag spec text for fail-closed sandboxing and extension normalization; wording still needs alignment with blocklist behavior.
plans/IMPLEMENTATION.md Documents fail-closed _contentRoot requirement and clarifies case-insensitive extension normalization.
internal/template/liquid_test.go Adds/adjusts inline-tag tests for _contentRoot requirement and mixed-case extension behavior.
internal/template/inline.go Implements fail-closed behavior when _contentRoot is missing, ensuring sandbox is always enforced.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/template/inline.go
Comment thread plans/PLAN.md

@zeroedin zeroedin left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Results

Scope: internal/template/inline.go, internal/template/liquid_test.go, plans/PLAN.md, plans/IMPLEMENTATION.md
Intent: Implement fail-closed sandbox for the {% inline %} tag (#932) and confirm case-insensitive extension handling (#931). One-line guard inversion: if contentRoot != ""if contentRoot == "" → error.
Mode: Interactive
PR type: Developer implementation PR — tests from spec PR #933.

Review team:

  • correctness (always)
  • testing (always)
  • maintainability (always)
  • project-standards (always)
  • agent-native (always)
  • learnings (always)
  • security — path-traversal sandbox behavior change

Verification

All 10 inline tag tests pass with -race. 333 total template specs green.

Test Status
returns error when _contentRoot is not set (#932) ✅ GREEN (was red before this PR)
matches file extensions case-insensitively (#931) ✅ GREEN
rejects binary file type with mixed-case extension (#931) ✅ GREEN
inlines an SVG file (updated with _contentRoot) ✅ GREEN
inserts content raw (updated with _contentRoot) ✅ GREEN
returns error when file not found (updated with _contentRoot) ✅ GREEN
returns error for binary file types ✅ GREEN
returns error for absolute paths ✅ GREEN
resolves parent directory paths within content root ✅ GREEN
rejects paths that escape the content directory ✅ GREEN

Findings

No findings across 7 reviewers. The implementation is a clean, minimal guard inversion that satisfies all spec tests.

Coverage

  • Testing gaps: none
  • Residual risks:
    • inline.go:76-92 — Duplicated contentDir/contentRoot validation pattern (fetch variable → empty check → error). Two instances in one function is not enough to justify extraction. Monitor for a third instance in future tags.
    • inline.go:94os.ReadFile follows symlinks, so a symlink inside contentRoot could point outside the sandbox. This is pre-existing and out of scope for this PR.
  • Suppressed: 0 findings
  • Failed reviewers: 0 of 7
  • No encoding/json usage detected
  • Conventional commit format verified

Verdict: Ready to merge

Reasoning: Clean implementation. The guard inversion at inline.go:85 is the exact one-line semantic change specified in IMPLEMENTATION.md. The sandbox is now unconditionally enforced — _contentRoot missing or empty produces an error instead of silently skipping path-traversal checks. All 333 template specs pass with -race. No correctness, security, maintainability, testing, or standards issues found across 7 independent reviewers. The two residual risks (validation pattern duplication, symlink traversal) are both pre-existing/deferred and do not affect this PR's mergeability.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@zeroedin zeroedin left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Results (Re-review)

Scope: internal/template/inline.go, internal/template/liquid_test.go, plans/PLAN.md, plans/IMPLEMENTATION.md
Intent: Fail-closed sandbox for {% inline %} tag (#932) + case-insensitive extension regression tests (#931). Merge commit bb2ce4b integrated main (which included PR #930 conformance changes).
PR type: Developer implementation PR

Prior Feedback Status

# Source File Status
1 Copilot inline.go:108 — symlink traversal ✅ Addressed — author skipped (pre-existing, out of scope). Correct.
2 Copilot plans/PLAN.md:1829 — allowlist/blocklist terminology Not addressed — author replied "stale after merge" but the text was not fixed. See finding #1.
3 Prior review All findings ✅ Clean — prior review found zero issues.

Re-verification

All 15 inline tag tests pass with `-race` (338 total specs, up from 333 after merge from main).

P2 -- Moderate

# File Issue Reviewer Confidence Route
1 plans/PLAN.md:1825 PLAN.md says strings.ToLower normalizes "before checking the binary-extension blocklist" but post-merge the code checks the allowlist first (inline.go:82: if !allowedExtensions[ext]). The blocklist is only a secondary error-message path. Fix: change "before checking the binary-extension blocklist" to "before checking the text-type allowlist" QA 100 safe_auto → review-fixer

Coverage

  • Testing gaps: none
  • Residual risks: same two pre-existing risks from prior review (symlink traversal, ..-prefixed directory false positive)
  • Merge conflict resolution verified clean — no logic drift in inline.go or liquid_test.go
  • Suppressed: 0 findings
  • Failed reviewers: 0

Verdict: Ready with fixes

Reasoning: The implementation is correct — the fail-closed guard at inline.go:101 works as intended. The merge from main (bb2ce4b) integrated cleanly and all 338 specs pass. One P2 fix needed: PLAN.md line 1825 references the "binary-extension blocklist" but the code now uses an allowlist as the primary extension check. The Copilot comment identified this; the author acknowledged it but didn't update the text.

@zeroedin

Copy link
Copy Markdown
Owner Author

Review response (re-review)

P2 #1 — PLAN.md "blocklist" → "allowlist" terminology

Skipped — architect scope. plans/PLAN.md is the architect's domain. Created issue #936 for the fix: change "before checking the binary-extension blocklist" to "before checking the text-type allowlist" on line ~1825.

@zeroedin zeroedin merged commit 7068940 into main Jul 11, 2026
5 checks passed
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.

inline tag: fail closed when _contentRoot is empty inline tag: add mixed-case extension test

2 participants