BUG-HUNT: [security] TemplateRenderer.render() silently swallows TemplateSecurityError and TemplateSizeError — security/DoS controls are completely bypassed on the public API #6573

Open
opened 2026-04-09 21:37:54 +00:00 by HAL9000 · 1 comment
Owner

Bug Report: [security] — TemplateRenderer.render() swallows security and size errors, returning raw template

Severity Assessment

  • Impact: Security controls in SecureTemplateRenderer are completely bypassed at the TemplateRenderer layer. A caller using TemplateRenderer (the exported convenience class) receives no indication that a security check or size limit was violated. A template with unsafe constructs (e.g. {obj.attr}) is simply returned unmodified, and no exception is raised. This violates the fail-safe principle: security failures should never be treated as success.
  • Likelihood: Any code path that instantiates TemplateRenderer directly (instead of SecureTemplateRenderer) and passes attacker-influenced content is affected. The TemplateRenderer is the primary convenience wrapper exported from renderer.py.
  • Priority: High

Location

  • File: src/cleveragents/templates/renderer.py
  • Class: TemplateRenderer
  • Lines: 49–61

Description

TemplateRenderer.render() wraps SecureTemplateRenderer.render() in a broad except TemplateError handler and returns the raw, unrendered template string on any error. Because TemplateSecurityError and TemplateSizeError are both subclasses of TemplateError, every security-critical exception is silently consumed.

This produces two concrete failure modes:

  1. Security bypass via TemplateSecurityError: A template containing {obj.attr}, {fn()}, or Jinja2 constructs triggers _check_security() inside SecureTemplateRenderer, which raises TemplateSecurityError. TemplateRenderer.render() catches it, logs a generic warning at WARNING level (without revealing which security rule was violated), and returns the raw template to the caller. The caller cannot tell whether rendering succeeded or was blocked.

  2. DoS protection bypass via TemplateSizeError: A template exceeding max_template_length (default 10 000 chars) triggers _check_template_length(), raising TemplateSizeError. Again the exception is caught, and the full oversized template string is returned. The size-limit DoS protection in SecureTemplateRenderer is rendered meaningless.

Evidence

# src/cleveragents/templates/renderer.py, lines 49–61

def render(self, template: str, context: dict[str, Any] | None = None) -> str:
    """Render *template* with *context*, returning raw on error."""
    context = context or {}
    try:
        return self._secure.render(template, context)
    except TemplateError:                   # ← catches TemplateSecurityError AND TemplateSizeError
        logger.warning(
            "Template rendering failed; returning raw template "
            "(length=%d). Enable DEBUG logging for details.",
            len(template),
        )
        logger.debug("Suppressed TemplateError for template: %r", template)
        return template                     # ← returns raw, potentially unsafe/oversized content

Concrete example of the security bypass:

renderer = TemplateRenderer()

# Template contains unsafe attribute access
result = renderer.render("{user.password}", {"user": some_obj})
# Expected: TemplateSecurityError raised (or propagated)
# Actual:   result == "{user.password}"  — raw template returned, no exception

The docstring says this is intentional ("preserve backward compatibility"), but returning the raw template on a security rejection silently violates the fail-safe principle and makes the SecureTemplateRenderer underneath useless when accessed through this wrapper.

Expected Behavior

TemplateSecurityError and TemplateSizeError should not be caught and suppressed by TemplateRenderer. At minimum the handler should re-raise security and size errors:

except TemplateSecurityError:
    raise                                   # never swallow security errors
except TemplateSizeError:
    raise                                   # never swallow DoS-protection errors
except TemplateValidationError:
    # backward-compat fallback acceptable here
    logger.warning(...)
    return template

Actual Behavior

Any template that trips a security or size check inside SecureTemplateRenderer is silently returned as-is by TemplateRenderer.render(). The caller gets True (well, a string) back and has no way to detect the failure without parsing the return value.

Suggested Fix

Split the exception handler in TemplateRenderer.render() to re-raise security-critical errors:

def render(self, template: str, context: dict[str, Any] | None = None) -> str:
    context = context or {}
    try:
        return self._secure.render(template, context)
    except (TemplateSecurityError, TemplateSizeError):
        raise  # fail-safe: never suppress security or DoS-protection errors
    except TemplateError:
        logger.warning(
            "Template rendering failed; returning raw template "
            "(length=%d). Enable DEBUG logging for details.",
            len(template),
        )
        logger.debug("Suppressed TemplateError for template: %r", template)
        return template

Category

security / error-handling

TDD Note

After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: @tdd_issue, @tdd_issue_<this-issue-number>, and @tdd_expected_fail to prove the bug exists before fixing it.


Automated by CleverAgents Bot
Supervisor: Bug Hunting | Agent: bug-hunter

## Bug Report: [security] — `TemplateRenderer.render()` swallows security and size errors, returning raw template ### Severity Assessment - **Impact**: Security controls in `SecureTemplateRenderer` are completely bypassed at the `TemplateRenderer` layer. A caller using `TemplateRenderer` (the exported convenience class) receives **no indication** that a security check or size limit was violated. A template with unsafe constructs (e.g. `{obj.attr}`) is simply returned unmodified, and no exception is raised. This violates the fail-safe principle: security failures should never be treated as success. - **Likelihood**: Any code path that instantiates `TemplateRenderer` directly (instead of `SecureTemplateRenderer`) and passes attacker-influenced content is affected. The `TemplateRenderer` is the primary convenience wrapper exported from `renderer.py`. - **Priority**: High ### Location - **File**: `src/cleveragents/templates/renderer.py` - **Class**: `TemplateRenderer` - **Lines**: 49–61 ### Description `TemplateRenderer.render()` wraps `SecureTemplateRenderer.render()` in a broad `except TemplateError` handler and returns the **raw, unrendered template string** on any error. Because `TemplateSecurityError` and `TemplateSizeError` are both subclasses of `TemplateError`, every security-critical exception is silently consumed. This produces two concrete failure modes: 1. **Security bypass via `TemplateSecurityError`**: A template containing `{obj.attr}`, `{fn()}`, or Jinja2 constructs triggers `_check_security()` inside `SecureTemplateRenderer`, which raises `TemplateSecurityError`. `TemplateRenderer.render()` catches it, logs a generic warning at WARNING level (without revealing *which* security rule was violated), and returns the raw template to the caller. The caller cannot tell whether rendering succeeded or was blocked. 2. **DoS protection bypass via `TemplateSizeError`**: A template exceeding `max_template_length` (default 10 000 chars) triggers `_check_template_length()`, raising `TemplateSizeError`. Again the exception is caught, and the **full oversized template string** is returned. The size-limit DoS protection in `SecureTemplateRenderer` is rendered meaningless. ### Evidence ```python # src/cleveragents/templates/renderer.py, lines 49–61 def render(self, template: str, context: dict[str, Any] | None = None) -> str: """Render *template* with *context*, returning raw on error.""" context = context or {} try: return self._secure.render(template, context) except TemplateError: # ← catches TemplateSecurityError AND TemplateSizeError logger.warning( "Template rendering failed; returning raw template " "(length=%d). Enable DEBUG logging for details.", len(template), ) logger.debug("Suppressed TemplateError for template: %r", template) return template # ← returns raw, potentially unsafe/oversized content ``` Concrete example of the security bypass: ```python renderer = TemplateRenderer() # Template contains unsafe attribute access result = renderer.render("{user.password}", {"user": some_obj}) # Expected: TemplateSecurityError raised (or propagated) # Actual: result == "{user.password}" — raw template returned, no exception ``` The docstring says this is intentional ("preserve backward compatibility"), but returning the raw template on a *security* rejection silently violates the fail-safe principle and makes the `SecureTemplateRenderer` underneath useless when accessed through this wrapper. ### Expected Behavior `TemplateSecurityError` and `TemplateSizeError` should **not** be caught and suppressed by `TemplateRenderer`. At minimum the handler should re-raise security and size errors: ```python except TemplateSecurityError: raise # never swallow security errors except TemplateSizeError: raise # never swallow DoS-protection errors except TemplateValidationError: # backward-compat fallback acceptable here logger.warning(...) return template ``` ### Actual Behavior Any template that trips a security or size check inside `SecureTemplateRenderer` is silently returned as-is by `TemplateRenderer.render()`. The caller gets `True` (well, a string) back and has no way to detect the failure without parsing the return value. ### Suggested Fix Split the exception handler in `TemplateRenderer.render()` to re-raise security-critical errors: ```python def render(self, template: str, context: dict[str, Any] | None = None) -> str: context = context or {} try: return self._secure.render(template, context) except (TemplateSecurityError, TemplateSizeError): raise # fail-safe: never suppress security or DoS-protection errors except TemplateError: logger.warning( "Template rendering failed; returning raw template " "(length=%d). Enable DEBUG logging for details.", len(template), ) logger.debug("Suppressed TemplateError for template: %r", template) return template ``` ### Category security / error-handling ### TDD Note After this bug issue is verified, a corresponding `Type/Testing` issue will be created for TDD. The test will use tags: `@tdd_issue`, `@tdd_issue_<this-issue-number>`, and `@tdd_expected_fail` to prove the bug exists before fixing it. --- **Automated by CleverAgents Bot** Supervisor: Bug Hunting | Agent: bug-hunter
HAL9000 added this to the v3.2.0 milestone 2026-04-09 21:43:03 +00:00
Author
Owner

Issue triaged by project owner:

  • State: Unverified
  • Priority: Critical — SECURITY VULNERABILITY: TemplateRenderer.render() silently swallows TemplateSecurityError. This means template injection attacks are silently ignored rather than blocked and reported.
  • Milestone: v3.2.0 — Security issues must be fixed in the earliest milestone
  • MoSCoW: Must Have — Silencing security errors is a critical vulnerability

Security Impact: If template security checks raise TemplateSecurityError but the error is swallowed, malicious template content (SSTI - Server Side Template Injection) could execute arbitrary code without any indication that an attack occurred. The error must be propagated to callers.


Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: project-owner

Issue triaged by project owner: - **State**: Unverified - **Priority**: Critical — **SECURITY VULNERABILITY**: `TemplateRenderer.render()` silently swallows `TemplateSecurityError`. This means template injection attacks are silently ignored rather than blocked and reported. - **Milestone**: v3.2.0 — Security issues must be fixed in the earliest milestone - **MoSCoW**: Must Have — Silencing security errors is a critical vulnerability **Security Impact**: If template security checks raise `TemplateSecurityError` but the error is swallowed, malicious template content (SSTI - Server Side Template Injection) could execute arbitrary code without any indication that an attack occurred. The error must be propagated to callers. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#6573
No description provided.