What happens
NotificationService.Webhooks.cs logs every outbound provider request at Information level, and it
redacts the URL with LogRedaction.RedactText(webhookUrl, LogRedaction.GetSensitiveValuesFromEnvironment()):
var redactedUrl = LogRedaction.RedactText(webhookUrl, LogRedaction.GetSensitiveValuesFromEnvironment());
_logger.LogInformation("Sending Pushover POST to {WebhookUrl} with body: {Body}", redactedUrl, redactedRequestBody);
RedactText replaces occurrences of values it finds in the process environment. A webhook URL the
user typed into Settings is not one of those, so nothing is replaced and the URL is written out
whole. The same call shape appears on the NTFY, Pushover, Telegram, Pushbullet, Slack, Discord and
generic branches.
Those URLs are the credential:
- Pushover:
https://api.pushover.net/1/messages.json?token=<app token>&user=<user key>
- Telegram:
https://api.telegram.org/bot<bot token>/sendMessage?chat_id=...
- Pushbullet:
https://api.pushbullet.com/v2/pushes?token=<access token>
- Discord and Slack: the webhook path itself is the bearer secret
The request bodies go through NotificationDiagnostics.AggressiveRedact, which is also
environment-sourced only, so the Pushover body's token= and user= fields are logged as well.
The codebase already has the right helper. LogRedaction.SanitizeUrl drops userinfo and the whole
query string, and the same file uses it two lines away on the failure path:
_logger.LogWarning("Pushover webhook URL missing 'token' or 'user' query parameter: {WebhookUrl}", LogRedaction.SanitizeUrl(webhookUrl));
So within one branch the warning is sanitised and the success line is not.
Why it matters
Information is the default level, so this is not a debug-only path. Anyone the operator sends a log
bundle to gets a working push credential for the operator's own devices, and container logs are
routinely shipped somewhere central.
Suggested shape
SanitizeUrl alone is not sufficient, because Telegram and Discord carry the secret in the path
rather than the query. A LogRedaction.SanitizeWebhookUrl that keeps scheme, host and port, drops
userinfo and query, and masks path segments for the known providers would cover all of them:
api.telegram.org/bot<token>/... becomes api.telegram.org/bot<redacted>/sendMessage
discord.com/api/webhooks/<id>/<token> keeps webhooks and masks the two segments after it
hooks.slack.com/services/... masks everything after services
- everything else keeps the path and loses the query
Then use it for the {WebhookUrl} argument on every logging call in
NotificationService.Webhooks.cs and in NotificationDiagnostics.LogFailedResponseAsync, and drop
the Pushover form body from the Information line rather than trying to redact it.
Sequencing note
NotificationService.Webhooks.cs already has two open pull requests against it, #754 and #943, so a
change here wants to land after those rather than beside them.
Disclosure: drafted with Claude Code at my direction; I read the cited code at the stated commit and reviewed this before posting.
What happens
NotificationService.Webhooks.cslogs every outbound provider request at Information level, and itredacts the URL with
LogRedaction.RedactText(webhookUrl, LogRedaction.GetSensitiveValuesFromEnvironment()):RedactTextreplaces occurrences of values it finds in the process environment. A webhook URL theuser typed into Settings is not one of those, so nothing is replaced and the URL is written out
whole. The same call shape appears on the NTFY, Pushover, Telegram, Pushbullet, Slack, Discord and
generic branches.
Those URLs are the credential:
https://api.pushover.net/1/messages.json?token=<app token>&user=<user key>https://api.telegram.org/bot<bot token>/sendMessage?chat_id=...https://api.pushbullet.com/v2/pushes?token=<access token>The request bodies go through
NotificationDiagnostics.AggressiveRedact, which is alsoenvironment-sourced only, so the Pushover body's
token=anduser=fields are logged as well.The codebase already has the right helper.
LogRedaction.SanitizeUrldrops userinfo and the wholequery string, and the same file uses it two lines away on the failure path:
So within one branch the warning is sanitised and the success line is not.
Why it matters
Information is the default level, so this is not a debug-only path. Anyone the operator sends a log
bundle to gets a working push credential for the operator's own devices, and container logs are
routinely shipped somewhere central.
Suggested shape
SanitizeUrlalone is not sufficient, because Telegram and Discord carry the secret in the pathrather than the query. A
LogRedaction.SanitizeWebhookUrlthat keeps scheme, host and port, dropsuserinfo and query, and masks path segments for the known providers would cover all of them:
api.telegram.org/bot<token>/...becomesapi.telegram.org/bot<redacted>/sendMessagediscord.com/api/webhooks/<id>/<token>keepswebhooksand masks the two segments after ithooks.slack.com/services/...masks everything afterservicesThen use it for the
{WebhookUrl}argument on every logging call inNotificationService.Webhooks.csand inNotificationDiagnostics.LogFailedResponseAsync, and dropthe Pushover form body from the Information line rather than trying to redact it.
Sequencing note
NotificationService.Webhooks.csalready has two open pull requests against it, #754 and #943, so achange here wants to land after those rather than beside them.
Disclosure: drafted with Claude Code at my direction; I read the cited code at the stated commit and reviewed this before posting.