fix(template): inline tag fail-closed sandbox and case-insensitive extension tests (#931, #932)#935
Conversation
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>
✅ Deploy Preview for alloyssg ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
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_contentRootis missing/empty (fail-closed sandbox). - Add spec tests verifying case-insensitive extension handling (
.SVGaccepted,.PNGrejected). - 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.
zeroedin
left a comment
There was a problem hiding this comment.
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— DuplicatedcontentDir/contentRootvalidation 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:94—os.ReadFilefollows symlinks, so a symlink insidecontentRootcould 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/jsonusage detected - Conventional commit format verified
Verdict: Ready to merge
Reasoning: Clean implementation. The guard inversion at
inline.go:85is the exact one-line semantic change specified in IMPLEMENTATION.md. The sandbox is now unconditionally enforced —_contentRootmissing 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
left a comment
There was a problem hiding this comment.
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.goorliquid_test.go - Suppressed: 0 findings
- Failed reviewers: 0
Verdict: Ready with fixes
Reasoning: The implementation is correct — the fail-closed guard at
inline.go:101works 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.
Summary
if contentRoot != ""guard in the inline tag's path-traversal sandbox to a hard error when_contentRootis missing from the render context. Previously the sandbox was silently skipped; now the tag fails closed.strings.ToLoweron the extension already handles case-insensitive matching. The new spec tests (.SVGaccepted,.PNGrejected) confirm this behavior and prevent regression ifToLowerwere removed.Implementation
One-line logic inversion in
internal/template/inline.go:Test results
All 333 template specs pass. Full suite green with
-race.Closes #931, closes #932
🤖 Generated with Claude Code