Educational developer tools & learning resources for software development & cybersecurity students — Learn more about our mission

Telegram Bot Security: Common Vulnerabilities and How to Build Safe Bots

Author Skand K. — Developer & Security Researcher Apr 02, 2026 10 min read 31 views
Telegram Bot Security: Common Vulnerabilities and How to Build Safe Bots

Educational Content: This article is written for educational purposes to help developers and cybersecurity students understand software concepts. Always follow ethical guidelines and applicable laws.

Telegram bots have become one of the most genuinely useful tools in a modern developer's toolkit. Payment notifications, customer support automation, order management panels, OTP delivery, server alerts, file sharing — if you've been building web products over the past few years, there's a reasonable chance a Telegram bot is already somewhere in your infrastructure. They're fast to build, they work reliably, and users actually engage with them because they don't require installing a separate app.

They're also one of the most consistently misconfigured pieces of infrastructure I encounter in code audits. When a Telegram bot is misconfigured, the consequences range from embarrassing to genuinely damaging — unauthorized commands being executed, entire reseller panels exposed, user data exfiltrated, or bots turned into tools for spam at scale.

This post covers the actual attack vectors targeting Telegram bots, why they work, and the specific fixes that close them. No vague advice — working PHP code you can implement today.

Understanding Why the Token Is Everything

When you create a bot through BotFather, you receive a bot token — a string that looks like 1234567890:ABCdef1234GHIjklmno5678pqrstUVWXyz. This token is the complete credential for your bot. There's no username and password separation, no 2FA, no session expiry. Anyone who possesses this token can do everything your bot can do — send messages to any user who has contacted it, read every message it has received, modify settings, add it to groups, and make API calls on its behalf.

This is the foundational security model you need to internalize, because virtually every Telegram bot security problem traces back to one root cause: the token ended up somewhere it shouldn't have been.

Common exposure vectors that are more prevalent than you'd expect: token hardcoded directly in source code that was pushed to a public GitHub repository (this happens constantly — searching GitHub for common Telegram token patterns returns thousands of results); token stored in a .env file that's inside the web root and accidentally served by the web server; token passed as a GET parameter in a webhook URL that appears in server access logs; token shared in a Telegram group or Discord channel during debugging and forgotten; token in a config file with world-readable permissions (644 instead of 600).

If your token has been exposed in any of these ways, go to BotFather and revoke it immediately, then continue reading for how to store the new one correctly.

Webhook Verification: Closing the Most Common Injection Vector

Most production bots use webhooks — Telegram sends an HTTPS POST request to your server when an update occurs (message received, button pressed, payment completed). This is efficient and real-time. It's also widely set up without any verification that the incoming request actually came from Telegram.

A webhook endpoint with no verification looks like this to an attacker: it's a publicly accessible URL that, when POSTed to with JSON data, executes actions — sends messages, processes orders, runs admin commands. The attacker doesn't need your bot token; they just need to discover the URL (server logs, robots.txt, directory enumeration, or just knowing your domain and guessing /bot/webhook.php). Then they can craft fake update payloads and trigger whatever your bot would normally do.

Telegram provides a clean solution: the secret token parameter. When setting up your webhook, provide a random secret string. Telegram will include this secret in every request it sends to your webhook in the X-Telegram-Bot-Api-Secret-Token header. Your webhook handler verifies this header before processing anything.

<?php
// Step 1: Set your webhook with a secret token
// Run this once to configure Telegram
$botToken = getenv('TELEGRAM_BOT_TOKEN');
$webhookUrl = 'https://yoursite.com/bot/webhook.php';
$secretToken = getenv('TELEGRAM_WEBHOOK_SECRET'); // stored securely, not in code

$result = file_get_contents(
    "https://api.telegram.org/bot{$botToken}/setWebhook" .
    "?url=" . urlencode($webhookUrl) .
    "&secret_token=" . urlencode($secretToken)
);
echo $result; // should show {"ok":true,"result":true}
<?php
// Step 2: Verify every incoming webhook request
$secretToken = getenv('TELEGRAM_WEBHOOK_SECRET');

$incomingToken = $_SERVER['HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN'] ?? '';

if (!hash_equals($secretToken, $incomingToken)) {
    http_response_code(403);
    // log the unauthorized attempt
    error_log('[Bot Security] Unauthorized webhook attempt from: ' . $_SERVER['REMOTE_ADDR']);
    exit('Forbidden');
}

// Only process after verification passes
$update = json_decode(file_get_contents('php://input'), true);
if (!$update) {
    exit;
}

// ... process the update

The hash_equals() function prevents timing attacks. The 403 response reveals nothing useful to an attacker. The log entry lets you monitor for probing attempts.

Input Validation: Treating Message Text as Untrusted Data

This one is easy to miss because Telegram messages feel like controlled input — users are clicking buttons and typing in a chat interface, so it seems like the inputs are constrained. They're not. Anyone can send arbitrary text to your bot by using the Telegram API directly or any API client. Message content should be treated with exactly the same skepticism as form POST data from a web browser.

The dangerous pattern is using message text directly in command processing, file operations, or database queries without validation. Imagine a bot that accepts an order ID from users:

<?php
// Dangerous - no validation
$message_text = $update['message']['text'];
$stmt = $db->query("SELECT * FROM orders WHERE id = $message_text");
// attacker sends: "1; DROP TABLE orders--"

Prepared statements prevent the SQL injection, but you should still validate format before touching the database:

<?php
function processOrderLookup($message_text, $chat_id, $db) {
    // Validate format before anything else
    $order_id = trim($message_text);
    if (!preg_match('/^d{1,10}$/', $order_id)) {
        sendMessage($chat_id, "Please send a valid order ID (numbers only).");
        return;
    }
    
    $order_id = (int) $order_id;
    
    // Prepared statement with ownership check
    $stmt = $db->prepare(
        "SELECT order_number, status, total, created_at 
         FROM orders 
         WHERE id = ? AND telegram_user_id = ?"
    );
    $stmt->execute([$order_id, $update['message']['from']['id']]);
    $order = $stmt->fetch(PDO::FETCH_ASSOC);
    
    if (!$order) {
        sendMessage($chat_id, "Order not found.");
        return;
    }
    
    sendMessage($chat_id, formatOrderDetails($order));
}

// For command handling, always use a whitelist
$allowed_commands = ['/start', '/help', '/orders', '/balance', '/support'];
$command = strtolower(explode(' ', trim($message_text))[0]); // get just the command part

if (!in_array($command, $allowed_commands, true)) {
    sendMessage($chat_id, "Unknown command. Send /help for available commands.");
    exit;
}

Privilege Escalation Prevention in Group Bots

If your bot has admin commands and operates in a group chat, privilege escalation is a real and underappreciated risk. The attack pattern is straightforward: a user joins any group your bot is in and sends a command like /makeadmin 12345678 or /setprice 0.01. If your bot only checks whether the user is in the group (or doesn't check at all) and not whether they're specifically authorized to run that command, it will execute it.

Always maintain an explicit whitelist of Telegram user IDs authorized to run privileged operations. Never derive authorization from data in the message payload itself:

<?php
// Hardcode or load from database/config
// The critical thing: don't let these IDs come from message content
define('ADMIN_TELEGRAM_IDS', [123456789, 987654321]);

function requireAdmin($sender_id, $chat_id) {
    if (!in_array($sender_id, ADMIN_TELEGRAM_IDS, true)) {
        sendMessage($chat_id, "You don't have permission to use this command.");
        error_log('[Bot Security] Unauthorized admin command attempt by user: ' . $sender_id);
        return false;
    }
    return true;
}

$sender_id = $update['message']['from']['id'];
$chat_id = $update['message']['chat']['id'];
$message_text = trim($update['message']['text'] ?? '');

if (str_starts_with($message_text, '/broadcast')) {
    if (!requireAdmin($sender_id, $chat_id)) {
        exit;
    }
    // process broadcast command
}

if (str_starts_with($message_text, '/setprice')) {
    if (!requireAdmin($sender_id, $chat_id)) {
        exit;
    }
    // process price command
}

For bots with role-based access rather than just admin/non-admin, load roles from your database using the sender's Telegram user ID as the lookup key. Never trust role information that arrives in the message payload.

Rate Limiting: Protecting Against Abuse and DoS

A Telegram bot with no rate limiting is trivially easy to abuse. Someone writes a five-line script that sends your bot a thousand messages per minute. Your database gets hammered, your hosting gets rate-limited or suspended, your API quota with third-party services gets burned through, and legitimate users can't use the bot while the attack is happening.

<?php
function checkBotRateLimit($user_id, $db, $maxRequests = 10, $windowSeconds = 60) {
    // Clean up old records occasionally (every ~5% of requests)
    if (rand(1, 20) === 1) {
        $db->prepare(
            "DELETE FROM bot_rate_limits WHERE window_start < NOW() - INTERVAL ? SECOND"
        )->execute([$windowSeconds * 2]);
    }
    
    $stmt = $db->prepare(
        "SELECT request_count FROM bot_rate_limits 
         WHERE user_id = ? AND window_start > NOW() - INTERVAL ? SECOND"
    );
    $stmt->execute([$user_id, $windowSeconds]);
    $row = $stmt->fetch();
    
    if ($row && $row['request_count'] >= $maxRequests) {
        return false;
    }
    
    $db->prepare(
        "INSERT INTO bot_rate_limits (user_id, request_count, window_start) 
         VALUES (?, 1, NOW())
         ON DUPLICATE KEY UPDATE request_count = request_count + 1"
    )->execute([$user_id]);
    
    return true;
}

$sender_id = $update['message']['from']['id'] ?? null;

if (!$sender_id) {
    exit;
}

if (!checkBotRateLimit($sender_id, $db)) {
    sendMessage($chat_id, "You're sending commands too quickly. Please wait a moment.");
    exit;
}

For high-volume bots, use Redis instead of MySQL for rate limiting — it's orders of magnitude faster and adds minimal latency to each request. A Redis rate limiter for a Telegram bot typically adds under 1ms overhead per request even under load.

Securing the Bot Token: Storage and Access

Your bot token should never appear in your source code — not even in a private repository. Private repositories get misconfigured, collaborators' accounts get compromised, repositories get accidentally made public, and repository history persists even after you remove a file.

On a PHP + shared hosting setup (Hostinger, cPanel, etc.):

<?php
// config.php - load from environment, never hardcode
define('BOT_TOKEN', getenv('TELEGRAM_BOT_TOKEN'));
define('WEBHOOK_SECRET', getenv('TELEGRAM_WEBHOOK_SECRET'));

if (!BOT_TOKEN) {
    error_log('[Critical] TELEGRAM_BOT_TOKEN environment variable not set');
    exit;
}

Set the environment variables in your hosting control panel under PHP settings or environment variables. If your host doesn't support environment variables, store them in a .env file outside the web root and load it with a minimal custom loader:

<?php
// Simple .env loader - place this file OUTSIDE public_html
// Path: /home/username/.env (not /home/username/public_html/.env)
function loadEnv($path) {
    if (!file_exists($path)) return;
    $lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    foreach ($lines as $line) {
        if (strpos($line, '#') === 0) continue;
        if (strpos($line, '=') !== false) {
            [$key, $value] = explode('=', $line, 2);
            putenv(trim($key) . '=' . trim($value));
        }
    }
}

loadEnv('/home/yourusername/.env'); // absolute path outside web root

If you must store the config inside the web root (strongly not recommended), add explicit server-level protection:

# .htaccess in the same directory as .env
<Files ".env">
    Order allow,deny
    Deny from all
</Files>

<Files "config.php">
    Order allow,deny
    Deny from all
</Files>

Set file permissions to 600 (owner read/write only): chmod 600 .env config.php

What to Do When Your Token Gets Leaked

Act immediately and in this order. First, go to BotFather, find your bot, and use the /revoke command. This invalidates the existing token instantly and generates a new one. Anyone using the old token for attacks gets cut off. Second, update your webhook configuration and all production systems with the new token. Third, audit your server logs for any requests made with the compromised token — look for webhook calls from unexpected IPs or unusual API calls. Fourth, rotate every other credential stored in the same location as the bot token, because if the token was exposed, everything else in that config file or repository should be considered compromised. Fifth, if your bot handles user data, assess whether the exposure constitutes a data breach under applicable regulations and act accordingly.

Putting It All Together

None of the issues covered here are exotic. Webhook verification, input validation, privilege checks, rate limiting, proper token storage — these are fundamentals that often get skipped when a bot is built quickly as a side feature of a larger project. The bot works, so it ships without the security basics.

The cost of adding all of these protections to a new Telegram bot is a few extra hours of careful implementation. The cost of not adding them — a compromised bot, a leaked seller panel, user data exposed, third-party API quotas burned — is significantly higher, and it often happens at the worst possible time.

Build the extra hour in. You won't regret it. — Skand K.

Share this article

Skand K. — Developer & Security Researcher — Author
Written by

Skand K. — Developer & Security Researcher

Senior Developer & Security Educator

Full-stack software engineer with 5+ years of experience in web development, mobile application architecture, and cybersecurity education. Passionate about teaching developers secure coding practices through hands-on, real-world projects. Contributor to open-source tools and author of educational guides on Telegram bot development, PHP frameworks, and Android security.