Skip to content

Security: rmaytr/CanvasAssignmentTrackingBot

Security

SECURITY.md

Security

Canvas access tokens skip multi-factor sign-in and carry the full permissions of the student who created them, so this project treats them like passwords. This page covers how to report a problem, what the bot stores, and the threats it's designed against.

Reporting a vulnerability

Please don't open a public issue for security problems. Use GitHub's private reporting: the Security tab → Report a vulnerability, on this repository.

Please include what you found, how to reproduce it, and its impact. You'll get a reply within 7 days. Fixes for confirmed issues are released as soon as possible and credited in the changelog unless you'd rather stay anonymous.

Version Supported
0.2.x (friends mode) ✅
0.1.x (single-user MVP) ❌, please upgrade

What the bot stores

Data Where Protection Retention
Canvas access token registry.db Fernet-encrypted (AES-128-CBC + HMAC-SHA256); key in a separate Docker secret Until /disconnect, /remove, or replaced (or the user switches to the calendar feed)
Canvas calendar feed link (feed mode, instead of a token) registry.db Fernet-encrypted, like a token: the link itself is the secret Until /disconnect, /remove, or replaced (or the user switches to a token)
Telegram bot token, encryption key secrets/*.txt on the host → Docker secrets File mode 600, owned by the container user; never in env vars, logs or the database Owner-managed
Canvas address, time zone, morning time, quiet hours, display name registry.db Server disk Until the user leaves
Assignment titles, due dates, status, course names, grade weights users/<telegram id>.db (one file per user) Server disk, readable only by that user's tenant Items purged 30 days past due
Message history (keys, timestamps, item references) Same per-user file No message text is stored 60 days
Invite codes registry.db Only the SHA-256 hash is stored; one-time use; 7-day expiry Kept as a record of use

Not stored: names, emails or IDs from the Canvas profile (only its time zone is read, and only during sign-up), grades or scores, submission contents, or the text of any message.

Threat model

# Threat Mitigations Tested by
T1 One user reads or changes another user's data Separate database file and tenant per user; every update routed by the sender's Telegram ID; button callbacks resolved only in the sender's own file; /users exposes sync health only test_friends_only_see_and_touch_their_own_items
T2 A stranger uses the bot Invite-only: one-time 96-bit codes stored as hashes, 7-day expiry, MAX_USERS cap; private chats only; unknown users' messages dropped without a reply; owner pairing code limited to 5 tries and 15 minutes test_gate_drops_strangers…, test_strangers_need_an_invite…
T3 A token or feed link leaks through the chat The message containing a token or a calendar feed link is deleted at any step (also inside /feedback, which then isn't forwarded); the bot says so only if deletion worked; the bot never repeats either test_a_token_pasted_at_any_step_is_deleted, test_failed_delete_is_not_claimed, test_feed_link_pasted_anywhere_is_deleted_but_not_used
T4 A token or feed link leaks through logs or errors Redaction of known secrets, tokens and feed links learned at runtime, and token-shaped and /feeds/calendars/… patterns (in messages, arguments and tracebacks); httpx/httpcore logs at WARNING; startup errors never echo values test_pasted_token_never_leaks, test_secrets_never_leak (sentinel tokens through the whole stack), test_feed_links_are_redacted_from_logs
T5 The database is copied Tokens are Fernet ciphertext; the key is a separate secret outside the data volume test_round_trip_and_ciphertext_hides_the_token, leak tests scan every .db file
T6 The token is sent somewhere other than Canvas Client is GET-only to /api/v1/ on one pinned host; redirects refused; cross-host pagination refused; HTTPS port 443 only. The feed client sends no token at all, fetches only https://<that host>/feeds/calendars/….ics, follows redirects only within that host, and stops at 5 MB (gzip is unpacked by the bot, so the limit holds while unpacking; stacked encodings refused). API responses are capped too (10 MB a page, 40 MB / 100 pages a request) test_canvas_client.py, test_feed_source.py
T7 The bot is pointed at internal hosts (SSRF) IP literals, localhost, .local/.internal/.lan and non-443 ports rejected, for Canvas addresses and feed links alike; the host must resolve only to public addresses at sign-up and again before every sync and check (a pass is cached for 5 minutes) test_canvas_url_rejects…, test_bad_addresses_are_refused, test_normalize_feed_url_refuses_everything_else, test_feed_on_a_private_host_is_refused_before_any_request
T8 Container escape or tampering Non-root UID 10001, read-only root filesystem, cap_drop: ALL, no-new-privileges, memory cap, no inbound ports Verified by running the image under compose.yaml
T9 Supply-chain compromise uv.lock with hashes; base image pinned by digest; GitHub Actions pinned to commit SHAs; pip-audit in CI; Dependabot CI audit job
T10 Lost or replaced encryption key Detected at start-up; the owner is told; users are asked to re-paste instead of being told Canvas rejected them; the installer refuses to create a new key while old data exists test_wrong_token_key_is_not_reported…
T11 One user degrades service for others Per-tenant ticks never wait on each other; per-request and per-sync timeouts; concurrent update handling with per-user ordering; responses from a user-chosen host are size-capped while unpacking; the feed parser is linear (bounded nesting and text lengths), has no backtracking regex, and runs in a worker thread test_slow_friend_does_not_hold_up_the_others, test_deep_nesting_parses_in_linear_time, test_split_summary_is_linear_on_long_runs_of_spaces, test_a_gzip_bomb_is_refused_without_unpacking_it_all

Known limitations

  • The owner can technically read tokens. Whoever runs the bot holds both the database and the key. Friends are told this plainly during sign-up. A public release would need per-user key wrapping and a hosted key service.
  • DNS rebinding: the address check runs before every sync and check, but a pass is reused for 5 minutes, so a host re-pointed inside that window could be reached until it expires. Impact is limited: HTTPS with certificate verification, no response content is echoed, and nothing listens inside the container.
  • Rate limits are in memory: at most 5 token or feed checks per user per 15 minutes and one /sync a minute; a restart resets them.
  • Canvas API policy: asking other people to paste personal tokens into a shared app conflicts with Instructure's API policy (see the README). Friends mode is limited to a small, invite-only beta for that reason. Calendar-feed mode avoids tokens entirely and is the recommended way to join a shared bot.
  • A feed link can't always be revoked. It only shows calendar items (titles, dates, links), not grades or submissions, but Canvas doesn't give every student a way to reset it. Anyone who has the link can read that calendar until it changes.

If a token leaks

  1. The user: in Canvas, go to Account → Settings → Approved Integrations and delete the token (and any you don't recognize). Then create a new one and paste it via /settings → 🔑 Canvas token.
  2. The owner: if the database and secrets/token_key.txt may have been copied together, treat every token as leaked. Ask everyone to delete and replace theirs, then rotate the key (stop the bot, back up, generate a new key, and have users re-paste).
  3. A calendar feed link leak: it exposes only the calendar (titles, due dates, links), not grades or submissions, and can't be used to act in Canvas. If your Canvas has a way to reset the feed, use it and paste the new link via /settings → 📅 Calendar feed; otherwise ask your school's Canvas admin.
  4. A Telegram bot token leak: send /revoke to @BotFather, put the new token in secrets/telegram_bot_token.txt and restart.

There aren't any published security advisories