AntFleet

Anatomy · eb14fff2-1

Telegram getUpdates acknowledges all updates (including other chats), risking message loss outside target chat

mediumdata-lossclosed in 4b9b492
repo 6f7fc663·PR #2·reviewed 1 week ago·closed 1 week ago

The vulnerable code

.github/workflows/messages.yml:65-75

65 if [ -n "$TEXT" ] && [ "$CHAT" = "$TELEGRAM_CHAT_ID" ]; then
66 MESSAGES=$(echo "$MESSAGES" | jq --arg src "telegram" --arg txt "$TEXT" '. + [{source: $src, text: $txt}]')
67 echo "Telegram: $TEXT"
68 fi
69 MAX_UPDATE_ID="$UPDATE_ID"
70 done
71
72 # Acknowledge all updates at once
73 if [ -n "$MAX_UPDATE_ID" ]; then
74 curl -s "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getUpdates?offset=$((MAX_UPDATE_ID + 1))" > /dev/null
75 fi

The reasoning

Opus

Script injection via Telegram/Discord/Slack message text in poll job

criticalsecurityhigh
  • .github/workflows/messages.yml:138-144
  • .github/workflows/messages.yml:71-80
The audit article in this PR claims the Extract message and Run steps are now safe because they use env-var intermediaries for `inputs.message`. That is true for the `run` job. However, the upstream `poll` job still performs unsafe shell interpolation of attacker-controlled text. `TEXT` is set from `jq -r` (raw, unescaped) on `result[$i].message.text`, then passed as `-f message="$TEXT"` to `gh workflow run`. While the `gh` argument itself is quoted properly (so command injection through the `gh` invocation does not occur), the same `TEXT` is also echoed unquoted-safely via `echo "Telegram: $TEXT"` which is benign — but more importantly, the message body is later piped into Claude as a prompt (`PROMPT="...\"$MESSAGE\"..."` in the run job). Any external user able to post in the configured Telegram chat / Discord channel / Slack channel can deliver an arbitrary instruction string to the Claude agent which has access to `GH_GLOBAL`, `ANTHROPIC_API_KEY`, `ALCHEMY_API_KEY`, and write permissions to the repo. The article's documented fix only addresses shell-template injection (the `${{ }}` substitution layer); it does not address prompt injection or trust-boundary issues for the message content itself. This is a real, exploitable vulnerability that the PR's audit explicitly misses.

Recommendation

Either (a) restrict message processing to authenticated sender IDs (verify `from.id`/`author.id`/`user` against an allowlist before dispatching), (b) sandbox the Claude run with a far tighter allowedTools set that excludes outbound network and `gh`/`git`, or (c) drop secrets like ALCHEMY_API_KEY, COINGECKO_API_KEY, GH_GLOBAL from the Run step's env unless absolutely required. At minimum, document the trust model explicitly in the audit article rather than implying the workflow is now secure.

GPT-5

Telegram getUpdates acknowledges all updates (including other chats), risking message loss outside target chat

mediumdata-losshigh
  • .github/workflows/messages.yml:65-75
MAX_UPDATE_ID is updated for every update, not just those from the configured chat. Advancing the offset acknowledges and discards all pending updates globally for the bot, potentially dropping messages from other chats that this workflow does not process.

Recommendation

Either dedicate the bot token to a single chat, or process/forward all chats. If multiple chats must be preserved, consider maintaining a separate consumer that handles all updates or redesign to not advance offset beyond the highest update actually processed (note: Telegram uses a global offset; selective ack is not supported, so multi-chat bots require processing for all chats to avoid loss). Document this constraint explicitly if keeping current behavior.

The agreement

Both frontier models flagged this within the same line range. AntFleet's unanimous gate fired — the finding posted on the PR. Closed in 4b9b492.

The fix

65 PREV_HOUR=$(( (HOUR + 23) % 24 ))
66
67 # Cron field matcher — supports: *, N, N-M (ranges), N,M (lists), */N, N/step, N-M/step (steps)
68 cron_match() {
69 local field="$1" value="$2"
70 [ "$field" = "*" ] && return 0
71 # Step values: */5 or 1-10/2
72 if [[ "$field" == */* ]]; then
73 local base="${field%/*}" interval="${field#*/}"
74 if [ "$base" = "*" ]; then
75 [ $((value % interval)) -eq 0 ] && return 0

Closure

Closed 1 week ago

SHA: 4b9b49251c8c9808bf147d55aa2930352af2e8c0

View closure receipt on GitHub →

Tweet thread template

tweet 1 of 8198 / 280

Two frontier models reviewed PR #2 on 6f7fc663. Both found this bug: medium data-loss: Telegram getUpdates acknowledges all updates (including other chats), risking message loss outside target chat

tweet 2 of 8122 / 280

The vulnerable code (.github/workflows/messages.yml:65-75): (full snippet at https://www.antfleet.dev/anatomy/eb14fff2-1)

tweet 3 of 8280 / 280

What Opus saw: "The audit article in this PR claims the Extract message and Run steps are now safe because they use env-var intermediaries for `inputs.message`. That is true for the `run` job. However, the upstream `poll` job still performs unsafe shell interpolation of attacke…

tweet 4 of 8275 / 280

What GPT-5 saw: "MAX_UPDATE_ID is updated for every update, not just those from the configured chat. Advancing the offset acknowledges and discards all pending updates globally for the bot, potentially dropping messages from other chats that this workflow does not process."

tweet 5 of 897 / 280

Both flagged the same line range. AntFleet's unanimous gate fired — the finding posted on the PR.

tweet 6 of 893 / 280

The fix landed in commit 4b9b492: (view diff at https://www.antfleet.dev/anatomy/eb14fff2-1)

tweet 7 of 881 / 280

AntFleet reviews every PR with two frontier models. Only unanimous findings post.

tweet 8 of 877 / 280

Full anatomy + reasoning + diffs: https://www.antfleet.dev/anatomy/eb14fff2-1

Paste into X composer one tweet at a time. X has no multi-tweet intent API.