Skip to content

Add a json template helper for webhook bodies - #127

Merged
LarsLaskowski merged 2 commits into
mainfrom
claude/issue-109-lgh0a5
Aug 23, 2026
Merged

Add a json template helper for webhook bodies#127
LarsLaskowski merged 2 commits into
mainfrom
claude/issue-109-lgh0a5

Conversation

@LarsLaskowski

@LarsLaskowski LarsLaskowski commented Aug 23, 2026

Copy link
Copy Markdown
Owner

📖 Description

Custom webhook bodies are rendered with text/template and sent with
Content-Type: application/json by default, but the template had no
JSON-escaping helper. Any value interpolated into a JSON string literal
(e.g. {"text": "{{.Message}}"}) was inserted raw. .Message embeds
ev.Resource verbatim, and for a disk alert that resource is a mountpoint
read from /proc/mounts — a value that can (in unusual but real cases)
contain a quote or backslash, producing a malformed request body.

This is low severity in practice (mountpoints aren't attacker-controlled
remote input; the realistic outcome is a broken webhook, not a
compromise), but it's a real robustness/correctness gap and the fix is
small.

This registers a json function on the webhook template's FuncMap:

"json": func(v any) (string, error) {
    b, err := json.Marshal(v)
    if err != nil {
        return "", err
    }
    return string(b), nil
},

so template authors can write {{json .Message}} (which supplies its own
quotes) instead of "{{.Message}}". Adding a function to the FuncMap
cannot break an existing template — templates that don't call json
render exactly as before.

Updated the shipped example config
(packaging/pimonitor.example.yaml) and docs/ARCHITECTURE.md's "Alert
delivery" section to document and recommend the safe form.
docs/API.md doesn't repeat the webhook template example, so no change
was needed there.

Deliberately out of scope (per the issue): switching to html/template
or auto-escaping/auto-detecting JSON bodies, and parseMounts not
decoding /proc/mounts octal escapes (a separate, distinct bug).

🎫 Issues

Closes #109

👩‍💻 Reviewer Notes

The core change is in internal/alert/notify.go: the new templateFuncs
var and .Funcs(templateFuncs) added to the template parse call in
NewNotifier. Everything else is tests/docs.

📑 Test Plan

Added to internal/alert/notify_test.go, extending the existing
template-rendering tests in their established style:

  • TestNotifier_JSONHelperEscapesHostileResource — an event with
    Resource: "/mnt/we\"ird\\path" rendered through
    {"text": {{json .Message}}} unmarshals cleanly and round-trips the
    expected message.
  • TestNotifier_UnescapedTemplateProducesInvalidJSON — the same hostile
    event through the old unescaped form {"text": "{{.Message}}"}
    produces invalid JSON, documenting why the helper exists.
  • TestNotifier_JSONHelperHandlesNonStringValues{{json .Value}}
    (float64) renders as a bare number and {{json .At}} (time.Time) as a
    quoted RFC 3339 string.
  • Backwards compatibility for templates that don't use json is already
    covered by the existing TestNotifier_RendersTemplate and
    TestNotifier_CustomContentType.

Ran go build ./..., go vet ./..., go test ./... -race -cover
(all packages pass), and golangci-lint run (0 issues).

✅ Checklist

General

  • I have added/updated tests for my changes (go test ./... -race -cover passes locally).
  • go vet ./... and golangci-lint run are clean.
  • I have tested my changes.
  • I have read the CONTRIBUTING documentation and followed the project's code style guidelines.
  • I have updated ARCHITECTURE.md if this changes a documented design decision.

REST API / configuration / packaging

  • I have updated docs/API.md to reflect a REST API change. (not applicable — no API shape change; API.md doesn't document the template example)
  • No breaking change to /api/v1/... response shapes, or a new API version (/api/v2/...) was introduced instead.
  • I have updated README.md / packaging/pimonitor.example.yaml to reflect a new or changed configuration option.
  • I have updated packaging/install.sh or the systemd units if this changes installation/packaging, and kept the unprivileged/privileged service split intact (see SECURITY.md). (not applicable)

⏭ Next Steps

parseMounts not decoding /proc/mounts octal escapes (\040, \011, \042, \134) is a genuine but distinct bug, called out in the issue as deserving its own issue rather than being folded into this PR.

text/template does no escaping of its own, so a custom webhook template
whose body is JSON (e.g. `{"text": "{{.Message}}"}`) inserted a resource
string raw. A disk alert's .Resource is a mountpoint read from
/proc/mounts, which can contain a quote or backslash, producing a
malformed request body.

Register a `json` function on the template that marshals its argument
and supplies its own surrounding quotes, so authors write
`{{json .Message}}` instead of quoting the interpolation by hand.
Existing templates that don't use `json` are unaffected.

Update the shipped example config and the architecture doc to
recommend the safe form.

@LarsLaskowski LarsLaskowski 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.

Reviewed against the repo's Go/security/API conventions. go build ./..., go vet ./... and go test ./... -race -cover all pass on f636e26 (internal/alert at 92.8%).

One finding, inline on packaging/pimonitor.example.yaml. Everything else on the checklist is clean: no new dependencies, no /api/v1/... shape change, no exec.Command or /proc//sys surface touched, privilege split unaffected, and a template render error is already handled gracefully (logged and the event dropped in dispatch) rather than panicking.

Comment thread packaging/pimonitor.example.yaml Outdated
The example dropped the old "PiMonitor: " prefix, and neither it nor
the ARCHITECTURE.md paragraph showed how to combine literal text with
an escaped value. Someone restoring the prefix as
"PiMonitor: {{json .Message}}" would get invalid JSON, since json
supplies its own quotes and they nest inside the literal ones —
reproducing exactly the bug this PR fixes.

Show the printf composition form instead, which wraps the whole string
value in json and round-trips correctly.
@sonarqubecloud

Copy link
Copy Markdown

@LarsLaskowski
LarsLaskowski merged commit cca3bd4 into main Aug 23, 2026
5 checks passed
@LarsLaskowski
LarsLaskowski deleted the claude/issue-109-lgh0a5 branch August 23, 2026 09:57
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.

Webhook templates can emit malformed JSON: no escaping helper for text/template bodies

2 participants