Skip to content

fix: security vulnerabilities and formatting bug - #33

Merged
rezhajulio merged 3 commits into
mainfrom
fix/security-review-and-formatting-bug
Sep 14, 2026
Merged

rezhajulio merged 3 commits into
mainfrom
fix/security-review-and-formatting-bug

Conversation

@rezhajulio

Copy link
Copy Markdown
Owner

Summary

Fixes 8 security vulnerabilities (1 critical, 3 high, 4 medium) and 1 formatting bug found during a comprehensive security review.

Security Fixes

🔴 Critical

  • URL whitelist bypass via userinfo injectionis_url_whitelisted used parsed.netloc instead of parsed.hostname, allowing attackers to craft URLs like https://docs.python.org:x@evil.com/phish that pass the whitelist but redirect to malicious sites

🟠 High

  • Message edit bypass — All moderation handlers (anti_spam, bio_bait, duplicate_spam, guest_bot) only checked update.message, ignoring update.edited_message. Attackers could post benign messages and edit in spam
  • UTF-16 offset mismatch in URL extraction — Manual string slicing with Telegram entity offsets (UTF-16 code units) was misaligned on messages containing emojis. Now uses PTB native parse_entities()
  • Non-atomic increment_message_count race condition — Replaced Python-side read-modify-write with atomic SQL UPDATE ... SET x = x + 1

🟡 Medium

  • Path traversal in Telegram URL whitelistt.me/pythonid/../../scam passed the whitelist check
  • WAL pragma only applied to one connectionsynchronous=NORMAL is connection-scoped; replaced one-off connection with SQLAlchemy connect event listener
  • Admin command group spam — Non-admin users typing /check etc. in groups triggered visible error replies; now silently ignored
  • Scheduler infinite retry loop — Users with LEFT status caused perpetual restrict failures every 5 minutes

Bug Fix

  • Names with square brackets break Markdown formatting — e.g. Romado [1376015] produced broken [Romado [1376015]](tg://user?id=...) links. Now escapes [ and ] in get_user_mention_by_id and _format_person

Test Changes

  • Updated all extract_urls / has_non_whitelisted_link tests to mock parse_entities() instead of raw offset slicing
  • Added 4 regression tests (userinfo injection, path traversal, square bracket names, underscore names)
  • 1095 tests passing ✅ | ruff ✅ | mypy ✅

rezhajulio and others added 3 commits September 14, 2026 03:25
Security fixes:
- fix(security): URL whitelist bypass via userinfo injection (CRITICAL)
  Use parsed.hostname instead of parsed.netloc; reject URLs with userinfo
- fix(security): path traversal bypass in Telegram URL whitelist
  Normalize paths with posixpath.normpath before segment extraction
- fix(security): message edit bypass in all moderation handlers (HIGH)
  anti_spam, bio_bait, duplicate_spam, guest_bot now check edited_message
- fix(security): UTF-16 offset mismatch in URL entity extraction
  Use message.parse_entities() instead of manual string slicing
- fix(security): non-atomic increment_message_count race condition
  Use atomic SQL UPDATE for message_count like increment_new_user_violation
- fix(security): WAL synchronous pragma only applied to one connection
  Use SQLAlchemy connect event listener for all pooled connections
- fix(security): admin command error replies exploitable for group spam
  Check admin status before chat type to silently ignore non-admins
- fix(bug): scheduler infinite retry loop for departed users
  Handle ChatMemberStatus.LEFT same as BANNED

Bug fix:
- fix(formatting): names with square brackets break Markdown mentions
  Escape [ and ] in user_full_name before mention_markdown (e.g. Romado [1376015])
  Also fix _format_person in trust handler for consistency
Fixes verified defects found reviewing the branch's own changes.

Security:
- is_url_whitelisted: the posixpath.normpath traversal fix closed one
  direction (segment demoted) but opened the opposite (a non-whitelisted
  segment erased by ".." promotes a whitelisted one, e.g.
  t.me/scam_group/../pythonid). Now rejects any "." or ".." path segment
  outright instead of normalizing.
- get_user_mention_by_id / trust._format_person: the bracket-escaping fix
  was inverted. escape_markdown(version=1) already escapes "[" (not "]");
  the added .replace("[", ...) re-escaped it, producing "\\[" — Telegram
  renders that as a literal backslash plus an unescaped bracket, the
  exact failure the fix claims to prevent. Only "]" needed escaping.
- require_admin_dm_target: reordering the private-chat check below the
  admin check silenced the audit log for non-admin attempts in groups.
  Restored the log before the early return.

Correctness (edited_message support added dead-message counting bugs):
- duplicate_spam / new_user_spam / guest_bot: editing an already-seen
  message re-delivers it as update.edited_message. All three counted it
  again, so a typo fix could trip the duplicate-message threshold, and a
  probation or guest-bot violation could be counted twice from one
  message. All three now ignore edits of messages already handled.
- scheduler: ChatMemberStatus.LEFT was folded into the same branch as
  BANNED, deleting warning history for users who merely left. BANNED is
  terminal; LEFT is reversible, so deleting let a user reset their
  auto-restriction clock by leaving and rejoining before the next sweep.
  LEFT now skips restriction without deleting the warning.

Tests (several could not fail on the bug they claimed to cover):
- test_properties.py / test_telegram_utils.py: the escaping regression
  tests recomputed the buggy expression or used substring assertions
  that pass on both correct and double-escaped output. Both now assert
  the exact expected string.
- test_whitelist.py: extract_urls/has_non_whitelisted_link tests stubbed
  parse_entities/parse_caption_entities directly, so they never exercised
  real UTF-16 offset slicing or the [MessageEntity.URL] filter. Rebuilt
  against real telegram.Message objects; added the reverse-direction
  traversal case.
- Several "update without message" guard tests set only update.message
  = None, leaving update.edited_message an auto-truthy MagicMock, so the
  fallback picked that mock and the guard was never exercised. Added
  edited_message = None throughout, plus one new edited_message
  regression test per counting handler.

Also moves the WAL-mode log line after the first real connection instead
of logging success unconditionally at construction, and hoists a
function-local sqlalchemy import to module scope.

1098 tests pass, ruff and mypy clean. Every new/changed test was
mutation-checked against the pre-fix code or a reverted guard.
…d_skips_warning

An earlier edit displaced this assertion into the new edited_message
regression test, leaving the already-restricted test's own subject
assertion (must not re-restrict) unverified. Restored; mutation-checked
by removing the source's early-return guard, which now fails the test
as expected.
@rezhajulio
rezhajulio merged commit 4cf048b into main Sep 14, 2026
6 checks passed
@rezhajulio
rezhajulio deleted the fix/security-review-and-formatting-bug branch September 14, 2026 06:29
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.

1 participant