litty-logs is a logging library so it touches your log messages, webhook URLs, file paths, and terminal output. heres how we keep all that secure no cap
webhook URLs get validated at registration time β not at 3am when a batch tries to POST and you find out your config was bricked:
- empty/whitespace URLs? instant
ArgumentException, thats not it Uri.TryCreate()must succeed withUriKind.Absolute- scheme MUST be
httporhttpsβfile://,ftp://,gopher://and other sketchy schemes get yeeted immediately - this blocks SSRF attempts where a misconfigured URL could make your app hit internal endpoints or exfiltrate data
see: LittyWebhookExtensions.cs β validation happens in AddLittyWebhookLogs() before any services are registered
newlines in log messages could create fake log entries in text output (JSON format is already safe because Utf8JsonWriter escapes everything):
\r\n,\n, and\rin message text get replaced with spaces- fast path β if theres no newlines, zero extra allocations (the common case)
- exception stack traces are intentionally multiline (they get their own section after the log line)
- this blocks log injection attacks where an attacker could craft a message like
legit msg\n[π₯ info] [timestamp] [FakeCategory] injected entryto create fake log entries
see: LittyLogsFormatHelper.cs β FormatLogLine() sanitizes the message before appending
webhook messages get rendered by Matrix hookshot or Slack. malicious log messages could embed:
- XSS:
<script>alert('pwned')</script>β could execute JavaScript in web-based clients - tracking pixels:
β renders as an invisible image, leaks IP addresses - phishing links:
[click here](https://evil.com/steal-creds)β renders as a clickable link - formatting spam:
# HUGE TEXTor**bold spam**β disrupts the chat channel
litty-logs sends an html field in the webhook payload (hookshot prefers it over text for rendering). all message content goes through the tiny HtmlEscape() boundary that encodes <, >, &, ", and ' while preserving literal UTF-8 emojis. content renders as text instead of executable HTML or markdown, and exception stack traces get wrapped in <pre><code> blocks for proper monospace energy ππ₯
the text field (markdown fallback for clients without HTML support) uses \n\n paragraph breaks between messages so they render as separate blocks per the CommonMark spec
see: Formatters/MatrixPayloadFormatter.cs β MessageToHtml() method uses HtmlEscape()
Slack payloads use a top-level fallback with mrkdwn: false plus Block Kit plain_text objects for both the header and every log section. user-shaped markup never becomes active formatting no cap:
- no XSS risk β
<script>alert('pwned')</script>renders as literal text, not executable HTML - no tracking pixels β
renders as literal text, not an image - no phishing links β
[click here](url)renders as literal text, not a clickable link - surprise mentions like
<!channel>stay visible text instead of pinging the room Utf8JsonWriterhandles JSON serialization safely while emojis stay literal UTF-8 π₯- exact exception fences added by the shared logger are removed before plain-text delivery, while the multiline exception content survives
- Unicode-scalar truncation respects Slack's 150-character header, 3000-character section, and 50-block message limits π
the incoming webhook URL is the Slack credential. keep it in secret storage, never commit it, and remember Username only labels the message header β Slack controls the installed app identity. use the Slack incoming webhook setup for a safe local test-channel flow ππ₯
see: Formatters/SlackPayloadFormatter.cs β safe plain-text Block Kit without a Slack SDK dependency π₯
when your app uses the webhook sink, the HTTP client that sends webhook requests generates its own log entries (System.Net.Http, Microsoft.Extensions.Http, Polly). without filtering:
- HTTP client logs a trace message about the webhook request
- that message gets sent to... the webhook
- which triggers another HTTP request, which generates another log
- infinite recursion until everything is cooked π
plus those HTTP client logs might contain the webhook URL (which has your secret token in the path). thats not it
litty-logs routes all HTTP-related categories to NullLogger.Instance in the webhook provider:
System.Net.Http.*Microsoft.Extensions.Http.*Polly.*
this prevents both the infinite loop AND accidental token exposure no cap
see: LittyWebhookProvider.cs β IsHttpClientCategory() check in CreateLogger()
heads up: if you add OTHER logging providers alongside the webhook sink (like a file sink), those providers will still see HTTP client logs. filter those categories in your logging config if you dont want webhook URLs showing up in file logs
litty-logs treats these as developer-controlled configuration, same trust model as Serilog and other .NET logging libraries:
- webhook URLs β set by the developer in code or app config. validated for scheme and format but not for destination (your app might legit need to hit internal URLs). if your config source is untrusted thats a you problem bestie
- file sink paths β
FilePathis developer config. no path traversal protection because the developer controls where logs go, same asSerilog.Sinks.File - log directory β file rotation uses
File.Move()andFile.Delete()without symlink checks. if an attacker has filesystem access to your log directory you got bigger problems fr fr
found a vulnerability? dont yeet it in a public issue bestie. reach out privately:
- open a security advisory on GitHub
- or email the maintainer directly
we'll get back to you faster than a Polly retry with zero backoff no cap π