Skip to content

feat: add Credo check flagging raw/1 in HEEx templates#179

Merged
zorn merged 2 commits into
mainfrom
131-credo-check-raw-in-heex
Jul 9, 2026
Merged

feat: add Credo check flagging raw/1 in HEEx templates#179
zorn merged 2 commits into
mainfrom
131-credo-check-raw-in-heex

Conversation

@zorn

@zorn zorn commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Closes #131.

Problem

Sobelow's XSS.Raw check never fires on raw/1 used inside a ~H template, because a template's contents are an unparsed binary in the AST — the same reason a naive Credo/AST check would miss it too. So raw/1 in HEEx was invisible to our security tooling.

Solution

A custom Credo check, Flick.Credo.Check.RawInHeex, that walks the AST for ~H (sigil_H) nodes and scans their template text for raw/1 calls, flagging each as a potential XSS risk (with accurate line/column).

Legitimate uses opt out with a HEEx-native marker:

<%!-- credo:allow-raw sanitized by Flick.Markdown --%>
{raw(Flick.Markdown.render_to_html(@content))}

A HEEx-native marker is required because Credo's built-in # credo:disable-* comments only work as real Elixir comments and can't live inside a template sigil.

Changes

  • credo/checks/raw_in_heex.ex — the check (kept out of lib/ since it depends on Credo.Check, unavailable in :prod).
  • .credo.exs — loads the check via requires and enables it.
  • mix.exsmix precommit now runs credo --strict, so the guardrail fires in the normal workflow.
  • test/test_helper.exs — loads the check and starts Credo's supervision tree (runtime: false) so its unit tests can use Credo.Test.Case.
  • <.markdown> core component — rendered Markdown is the app's only sanctioned raw/1 use, now centralized in one component that carries the sole opt-out. Both LiveViews (viewer_live, vote_capture_live) use it, reducing the audited raw surface from two sites to one.

Verification

  • mix precommit green: format clean, credo --strict 0 issues (95 checks), 106 tests pass.
  • 7 dedicated unit tests for the check (Credo.Test.Case).
  • Confirmed end-to-end: removing a marker makes Credo report the raw/1 in HEEx warning; restoring it returns to green.

Known limitations (deliberate)

  • The scan is line-based, so a raw( split across multiple lines could slip through. Real call sites are single-line {raw(...)}; this is defense-in-depth, not a proof.
  • The regex also matches qualified calls like Phoenix.HTML.raw( — appropriate for the security intent.

Sobelow's XSS.Raw check never fires on `raw/1` used inside `~H` sigils
because a template's contents are an unparsed binary in the AST. This
adds a custom Credo check, `Flick.Credo.Check.RawInHeex`, that scans
`~H` sigil source for `raw/1` calls and flags them as a potential XSS
risk.

Legitimate uses opt out with a HEEx-native `credo:allow-raw` marker.
Credo's own `# credo:disable-*` comments only work as real Elixir
comments, so they cannot live inside a template. Rendered Markdown is
the app's only sanctioned `raw/1` use, so it is now centralized in a
single `<.markdown>` core component that carries the sole opt-out.

- Wire the check into `.credo.exs` (loaded via `requires`).
- Run `credo --strict` as part of `mix precommit`.
- Start Credo's supervision tree in the test helper so the check's unit
  tests can use `Credo.Test.Case`.
@zorn zorn changed the title Add Credo check flagging raw/1 in HEEx templates (#131) feat: add Credo check flagging raw/1 in HEEx templates Jul 9, 2026
@zorn
zorn requested a review from Copilot July 9, 2026 16:37

Copilot AI 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.

Pull request overview

This PR closes #131 by adding a custom Credo check to detect raw/1 usage inside ~H (HEEx) sigils—covering a blind spot where template contents are stored as an unparsed binary in the AST—and by centralizing the app’s sanctioned Markdown-to-HTML rendering behind a single component-level raw/1 call.

Changes:

  • Add Flick.Credo.Check.RawInHeex and enable it via .credo.exs requires + check configuration.
  • Ensure the check is exercised in CI/dev workflow by running credo --strict in mix precommit, and wiring Credo into the test environment for unit tests.
  • Replace direct raw(Flick.Markdown.render_to_html(...)) usage in LiveViews with a new <.markdown> core component that contains the sole opt-out marker.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
credo/checks/raw_in_heex.ex Implements the custom Credo check that scans ~H template text for raw/1 usage with an allow-marker escape hatch.
.credo.exs Loads the custom check via requires and enables it in the configured checks list.
mix.exs Updates mix precommit to run credo --strict so the new guardrail runs in the normal workflow.
test/test_helper.exs Ensures the custom check module is loaded for tests and starts Credo when needed for Credo.Test.Case.
test/flick/credo/check/raw_in_heex_test.exs Adds unit tests validating detection and allow-marker behavior for the new check.
lib/flick_web/components/core_components.ex Adds a reusable <.markdown> component that sanitizes Markdown output and contains the single approved raw/1 use + marker.
lib/flick_web/live/ballots/viewer_live.ex Switches ballot description rendering to use <.markdown> instead of calling raw/1 inline.
lib/flick_web/live/vote/vote_capture_live.ex Switches ballot description rendering to use <.markdown> instead of calling raw/1 inline.

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

Comment thread credo/checks/raw_in_heex.ex Outdated
]

@allow_marker "credo:allow-raw"
@raw_call ~r/\braw\s*\(/

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.

Good catch — tightened in 80265e4. The check now requires a HEEx interpolation opener before raw( on the line, so literal raw( in prose, HTML attributes, or <pre> code samples no longer produces a false positive.

One adjustment to the suggestion: requiring { alone would create a false negative for <%= raw(...) %> (valid HEEx body interpolation), which is worse for a security guardrail. So the regex matches either opener — (?:\{|<%=)[^{}]*\braw\s*\( — and I added regression tests for both the <%= %> form and for non-interpolated literal text like <div>raw(@x)</div>.

Require a HEEx interpolation opener (`{` or `<%= `) before `raw(` on
the line, so literal `raw(` in prose, HTML attributes, or `<pre>` code
samples is not flagged as a false positive (per PR review).

Requiring `{` alone would miss `<%= raw(...) %>`; matching either
opener keeps both interpolation forms covered. Adds regression tests
for the `<%= %>` form and for non-interpolated literal text.
@zorn
zorn merged commit 29247ed into main Jul 9, 2026
4 checks passed
@zorn
zorn deleted the 131-credo-check-raw-in-heex branch July 9, 2026 16:52
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.

It feels sus that sobelow is not yelling at me when I include raw in my heex template.

2 participants