mirror of
https://github.com/wassname/pi-telegram.git
synced 2026-06-27 16:16:14 +08:00
security: require pre-configured allowedUserId, remove auto-pair
The first-DM auto-pair behavior combined with ! shell passthrough meant
the first account to DM the bot gained arbitrary shell access. This
removes that footgun entirely.
- allowedUserId must be set before polling starts; missing config blocks
polling with a TUI warning rather than silently accepting any sender
- TELEGRAM_ALLOWED_USER_ID env var is read on session start and
overwrites the saved config value, so rotating the allowed user is a
restart away
- /telegram-setup now prompts for a numeric user ID after the bot token
if one is not already configured
- Denied senders receive an auth error reply; their numeric ID is also
logged to the pi TUI as a warning so operators can identify themselves
on a fresh install without needing @userinfobot
- Dropped the {kind: "pair"} authorization state entirely; undefined
allowedUserId now produces deny, not pair
- Removed pairTelegramUserIfNeeded, shouldPair, shouldNotifyPaired
Existing installs with allowedUserId already in telegram.json are
unaffected. Fresh installs require explicit configuration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,22 @@ export const TELEGRAM_BOT_TOKEN_ENV_VARS = [
|
||||
"TELEGRAM_KEY",
|
||||
] as const;
|
||||
|
||||
export const TELEGRAM_ALLOWED_USER_ID_ENV_VAR = "TELEGRAM_ALLOWED_USER_ID";
|
||||
|
||||
export function readAllowedUserIdFromEnv(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): number | undefined {
|
||||
const raw = env[TELEGRAM_ALLOWED_USER_ID_ENV_VAR]?.trim();
|
||||
if (!raw) return undefined;
|
||||
const parsed = Number(raw);
|
||||
if (!Number.isInteger(parsed) || parsed <= 0) {
|
||||
throw new Error(
|
||||
`${TELEGRAM_ALLOWED_USER_ID_ENV_VAR}="${raw}" is not a valid Telegram user ID (must be a positive integer)`,
|
||||
);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function getTelegramBotTokenInputDefault(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
configToken?: string,
|
||||
|
||||
+3
-27
@@ -95,7 +95,6 @@ export interface TelegramUpdateRoutingLike {
|
||||
}
|
||||
|
||||
export type TelegramAuthorizationState =
|
||||
| { kind: "pair"; userId: number }
|
||||
| { kind: "allow" }
|
||||
| { kind: "deny" };
|
||||
|
||||
@@ -103,9 +102,6 @@ export function getTelegramAuthorizationState(
|
||||
userId: number,
|
||||
allowedUserId?: number,
|
||||
): TelegramAuthorizationState {
|
||||
if (allowedUserId === undefined) {
|
||||
return { kind: "pair", userId };
|
||||
}
|
||||
if (userId === allowedUserId) {
|
||||
return { kind: "allow" };
|
||||
}
|
||||
@@ -214,14 +210,11 @@ export type TelegramUpdateExecutionPlan =
|
||||
| {
|
||||
kind: "callback";
|
||||
query: TelegramCallbackQueryLike;
|
||||
shouldPair: boolean;
|
||||
shouldDeny: boolean;
|
||||
}
|
||||
| {
|
||||
kind: "message";
|
||||
message: TelegramMessageLike & { from: TelegramUserLike };
|
||||
shouldPair: boolean;
|
||||
shouldNotifyPaired: boolean;
|
||||
shouldDeny: boolean;
|
||||
};
|
||||
|
||||
@@ -239,15 +232,12 @@ export function buildTelegramUpdateExecutionPlan(
|
||||
return {
|
||||
kind: "callback",
|
||||
query: action.query,
|
||||
shouldPair: action.authorization.kind === "pair",
|
||||
shouldDeny: action.authorization.kind === "deny",
|
||||
};
|
||||
case "message":
|
||||
return {
|
||||
kind: "message",
|
||||
message: action.message,
|
||||
shouldPair: action.authorization.kind === "pair",
|
||||
shouldNotifyPaired: action.authorization.kind === "pair",
|
||||
shouldDeny: action.authorization.kind === "deny",
|
||||
};
|
||||
}
|
||||
@@ -280,10 +270,7 @@ export interface TelegramUpdateRuntimeDeps {
|
||||
>,
|
||||
ctx: ExtensionContext,
|
||||
) => Promise<void>;
|
||||
pairTelegramUserIfNeeded: (
|
||||
userId: number,
|
||||
ctx: ExtensionContext,
|
||||
) => Promise<boolean>;
|
||||
onDeniedUserId?: (userId: number) => void;
|
||||
answerCallbackQuery: (
|
||||
callbackQueryId: string,
|
||||
text?: string,
|
||||
@@ -356,10 +343,8 @@ export async function executeTelegramUpdatePlan(
|
||||
return;
|
||||
}
|
||||
if (plan.kind === "callback") {
|
||||
if (plan.shouldPair) {
|
||||
await deps.pairTelegramUserIfNeeded(plan.query.from.id, deps.ctx);
|
||||
}
|
||||
if (plan.shouldDeny) {
|
||||
deps.onDeniedUserId?.(plan.query.from.id);
|
||||
const callbackQueryId = getTelegramCallbackQueryId(plan.query);
|
||||
if (callbackQueryId) {
|
||||
await deps.answerCallbackQuery(
|
||||
@@ -372,18 +357,9 @@ export async function executeTelegramUpdatePlan(
|
||||
await deps.handleAuthorizedTelegramCallbackQuery(plan.query, deps.ctx);
|
||||
return;
|
||||
}
|
||||
const pairedNow = plan.shouldPair
|
||||
? await deps.pairTelegramUserIfNeeded(plan.message.from.id, deps.ctx)
|
||||
: false;
|
||||
const replyTarget = getTelegramMessageReplyTarget(plan.message);
|
||||
if (pairedNow && plan.shouldNotifyPaired && replyTarget) {
|
||||
await deps.sendTextReply(
|
||||
replyTarget.chatId,
|
||||
replyTarget.messageId,
|
||||
"Telegram bridge paired with this account.",
|
||||
);
|
||||
}
|
||||
if (plan.shouldDeny) {
|
||||
deps.onDeniedUserId?.(plan.message.from.id);
|
||||
if (replyTarget) {
|
||||
await deps.sendTextReply(
|
||||
replyTarget.chatId,
|
||||
|
||||
Reference in New Issue
Block a user