Skip to content

fix: block eval of string BLOCK via template.new() (GH #245) - #418

Open
toddr-bot wants to merge 1 commit into
mainfrom
koan.toddr.bot/fix-eval-perl-bypass
Open

fix: block eval of string BLOCK via template.new() (GH #245)#418
toddr-bot wants to merge 1 commit into
mainfrom
koan.toddr.bot/fix-eval-perl-bypass

Conversation

@toddr-bot

@toddr-bot toddr-bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

What

Prevent arbitrary Perl code execution via template.new({BLOCK => '...'}) when EVAL_PERL is disabled.

Why

Fixes GH #245. Template::Document::new() calls eval $block on string BLOCK arguments — this runs regardless of the EVAL_PERL config. Since template and component are exposed as Document objects in the stash, a template author can call template.new({BLOCK => 'malicious code'}) to execute arbitrary Perl, bypassing the EVAL_PERL security control entirely.

The side effects run even though new() ultimately fails (at bless), because the eval on line 80 fires before the bless on line 94.

How

When new() is called as an instance method (detected via ref $class), string BLOCK and DEFBLOCKS arguments are rejected with an error before any eval occurs. This blocks the template-side attack vector while preserving class method calls — the only legitimate compilation path.

Testing

  • 11 new tests in t/eval_perl_bypass.t:
    • Class method with string BLOCK succeeds (compilation path)
    • Instance method with string BLOCK is blocked (no side effects)
    • Instance method with string DEFBLOCKS is blocked
    • End-to-end exploit via template.new() blocked
    • End-to-end exploit via component.new() blocked
  • Full test suite passes

Quality Report

Changes: 2 files changed, 122 insertions(+), 1 deletion(-)

Code scan: clean

Tests: passed (OK)

Branch hygiene: clean

Generated by Kōan

@toddr-bot

Copy link
Copy Markdown
Contributor Author

Recreated from #352 (auto-closed when the toddr-bot fork was removed). Original branch is now hosted on this repo.

@minusdavid

Copy link
Copy Markdown

What do we need to do to help this one move along? Really looking forward to this change.

@atoomic

atoomic commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

@Koan-Bot review

@Koan-Bot

Koan-Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

@minusdavid: What do we need to do to help this one move along? Really looking forward to this change.

Fix logic sound. Two blockers: test payloads use die (fails under eval anyway — tests pass without the fix), and DEFBLOCKS string values still get eval'd on instance calls. Fix those, merge-ready.

@Koan-Bot

Koan-Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Previous review — superseded by a newer review below.

@Koan-Bot Koan-Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Blocking issues found — see the review comment above.

Template authors could execute arbitrary Perl code by calling
template.new({BLOCK => '...'}) from within a template, bypassing
the EVAL_PERL security control. The eval happened in Document::new()
before the bless call, so side effects ran even though the method
ultimately failed.

Block string BLOCK (and DEFBLOCKS) eval when new() is called as an
instance method. The compilation pipeline always uses class method
calls, so this check has no impact on normal operation.
@toddr-bot
toddr-bot force-pushed the koan.toddr.bot/fix-eval-perl-bypass branch from 2914e40 to df4d86b Compare July 3, 2026 03:08
@toddr-bot toddr-bot changed the title fix: block eval of string BLOCK in instance method calls (GH #245) fix: block eval of string BLOCK via template.new() (GH #245) Jul 3, 2026
@minusdavid

Copy link
Copy Markdown

Cool I see the new commit is up which should satisfy the "1 requested change" from "Koan-Bot"?

What's next?

@atoomic

atoomic commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

@Koan-Bot review

@atoomic
atoomic requested a review from Koan-Bot July 3, 2026 20:39
@Koan-Bot

Koan-Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

@minusdavid: Cool I see the new commit is up which should satisfy the "1 requested change" from "Koan-Bot"?

Yes. New commit resolves both blocking findings. Tests now use real eval-succeeding payloads. DEFBLOCKS bypass closed with second guard. Verified sole compile caller (Provider.pm:925) uses class method — fix won't break it. Two minor suggestions remain, non-blocking. Merge-ready.

@Koan-Bot

Koan-Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

PR Review — fix: block eval of string BLOCK via template.new() (GH #245)

Merge-ready. The revised commit resolves every blocking finding from the prior review; only two minor portability/clarity nits remain.

What's solid:

  • The ref $class guard is surgical — it rejects only string BLOCK/DEFBLOCKS on instance calls, leaving legitimate class-method compilation (verified: Provider.pm:925 calls $DOCUMENT->new as a class method) and coderef-BLOCK instance calls untouched.

  • The DEFBLOCKS bypass flagged last round is now closed by a second guard inside the map, before the eval.

  • Tests now use payloads that succeed under eval (a file-creating open() + sub {"pwned"}), so they genuinely distinguish fixed-vs-unfixed code — the earlier die-based false-pass problem is gone. Side effects are asserted via a marker file, and both template.new() and component.new() are covered end-to-end.

  • Suggestion: DEFBLOCKS guard reuses the "BLOCK" error string (misleading debug text only).

  • Suggestion: test hardcodes /tmp, which weakens the side-effect assertion on non-Unix hosts.


🟢 Suggestions

1. DEFBLOCKS guard reuses the BLOCK error message
lib/Template/Document.pm:101

The guard inside the DEFBLOCKS map reports "cannot eval BLOCK in instance method call", but it fires for a string DEFBLOCKS value, not the main BLOCK.

Why it matters: if this path ever trips in real usage, the error text points a debugger at the wrong argument. Functionally the guard is correct — this is purely a clarity nit.

Consider a distinct message, e.g. "cannot eval DEFBLOCKS in instance method call".

return $class->error("cannot eval BLOCK in instance method call")
    if ref $class;
2. Hardcoded /tmp path reduces portability
t/eval_perl_bypass.t:15

The marker file is hardcoded to /tmp, which is not writable (or may not exist) on Windows and some CI sandboxes. If /tmp is unwritable, the exploit open() would silently fail even without the fix, weakening the negative assertion ok(!-e $marker).

Using File::Temp or File::Spec->tmpdir for the directory would make the test portable and keep the side-effect assertion meaningful everywhere:

use File::Temp qw( tempdir );
my $dir = tempdir( CLEANUP => 1 );
my $marker = "$dir/tt-eval-bypass-$$";

Not blocking — the test is correct on the Unix hosts TT primarily targets.

my $marker = "/tmp/tt-eval-bypass-test-$$";

Checklist

  • No hardcoded secrets
  • No unsafe eval/exec (BLOCK + DEFBLOCKS vectors closed)
  • Input validation at security boundary
  • Test coverage validates the fix (real eval payloads)
  • Tests verify behavior, not source inspection
  • Cross-platform test portability — suggestion #2

Automated review by Kōan (Claude · model opus) HEAD=df4d86b 3 min 19s

@Koan-Bot Koan-Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blocking issues found.

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.

4 participants