-
Notifications
You must be signed in to change notification settings - Fork 0
Security Guide
maule edited this page Aug 16, 2026
·
1 revision
Security best practices for building Telegram bots with TGbotPHP.
// WRONG - Token in GET parameter
$token = $_GET['token'];
// WRONG - Token hardcoded
$token = '123456789:ABCdefGHIjklmnoPQRstuvWXyz_ABCDE';
// WRONG - Token in version control
// file: bot.php (NEVER COMMIT)
define('TOKEN', 'your_token_here');// CORRECT - Environment variable
$token = getenv('TELEGRAM_BOT_TOKEN');
// CORRECT - PHP 8.1+ readonly property
readonly string $botToken = getenv('TELEGRAM_BOT_TOKEN');
// CORRECT - .env file (never committed)
// .env (in .gitignore)
TELEGRAM_BOT_TOKEN=your_token_here- Create
.envfile (add to.gitignore):
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklmnoPQRstuvWXyz_ABCDE
DEBUG_MODE=false
- Load in webhook:
<?php
// Load .env
$env = parse_ini_file(__DIR__ . '/.env');
$token = $env['TELEGRAM_BOT_TOKEN'];- Or use environment variables directly:
<?php
$token = $_ENV['TELEGRAM_BOT_TOKEN'] ?? getenv('TELEGRAM_BOT_TOKEN');Telegram requires HTTPS for webhooks.
<?php
// Check HTTPS
if (empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] === 'off') {
http_response_code(403);
error_log('HTTPS required');
exit('HTTPS required');
}RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]server {
listen 443 ssl http2;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# ... rest of config
}
server {
listen 80;
return 301 https://$server_name$request_uri;
}Always validate webhook data.
<?php
$updates = file_get_contents("php://input");
// Validate JSON
$decoded = json_decode($updates, true);
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400);
error_log('Invalid JSON: ' . json_last_error_msg());
exit;
}
// Validate Telegram IP (optional but recommended)
$allowedIps = [
'149.154.160.0/20',
'91.108.4.0/22',
];
$clientIp = $_SERVER['REMOTE_ADDR'];
if (!isIpInRange($clientIp, $allowedIps)) {
http_response_code(403);
error_log("Request from unauthorized IP: $clientIp");
exit;
}
$bot = new botTG(token: $token, updates: $updates);Add extra security layer (if available).
$secretToken = bin2hex(random_bytes(32));
// Store in .env: TELEGRAM_SECRET_TOKEN=abc123def456...curl -X POST "https://api.telegram.org/bot<TOKEN>/setWebhook" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-domain.com/webhook.php",
"secret_token": "your_secret_token"
}'<?php
$secretToken = getenv('TELEGRAM_SECRET_TOKEN');
$headerToken = $_SERVER['HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN'] ?? null;
if ($secretToken && $headerToken !== $secretToken) {
http_response_code(403);
error_log('Invalid webhook secret');
exit;
}Add HTTP security headers.
<?php
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('X-XSS-Protection: 1; mode=block');
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
header('Content-Security-Policy: default-src \'none\'');
header('Referrer-Policy: no-referrer');Or in .htaccess:
<IfModule mod_headers.c>
Header set X-Content-Type-Options "nosniff"
Header set X-Frame-Options "DENY"
Header set X-XSS-Protection "1; mode=block"
Header set Strict-Transport-Security "max-age=31536000"
Header set Content-Security-Policy "default-src 'none'"
</IfModule>Never expose sensitive information.
// WRONG - Exposes path and token in errors
error_reporting(E_ALL);
ini_set('display_errors', '1');// Production
error_reporting(E_ALL);
ini_set('display_errors', '0');
ini_set('log_errors', '1');
ini_set('error_log', '/var/log/bot-errors.log');
// Development
if (getenv('DEBUG_MODE')) {
error_reporting(E_ALL);
ini_set('display_errors', '1');
}Prevent directory traversal attacks.
// WRONG - Can access any file on system
$photo = $_GET['photo']; // "../../etc/passwd"
$bot->sendMessage($chatId, photo: $photo);// Define safe directory
define('PHOTOS_DIR', __DIR__ . '/photos/');
function getSafePhotoPath($filename) {
$path = realpath(PHOTOS_DIR . basename($filename));
// Verify path is within photos directory
if (!$path || strpos($path, PHOTOS_DIR) !== 0) {
throw new Exception('Invalid file path');
}
return $path;
}
// Usage
$photo = getSafePhotoPath($_GET['photo'] ?? 'default.png');Prevent spam and abuse.
<?php
class RateLimiter {
private array $limits = [];
private int $window = 60; // 1 minute
public function isAllowed($userId, $limit = 10): bool {
$now = time();
$key = "user_$userId";
if (!isset($this->limits[$key])) {
$this->limits[$key] = [];
}
// Remove old requests
$this->limits[$key] = array_filter(
$this->limits[$key],
fn($time) => $now - $time < $this->window
);
if (count($this->limits[$key]) >= $limit) {
return false;
}
$this->limits[$key][] = $now;
return true;
}
}
// Usage
$limiter = new RateLimiter();
if (!$limiter->isAllowed($bot->getChatId(), limit: 10)) {
$bot->sendMessage(
$bot->getChatId(),
"Too many requests. Please wait."
);
exit;
}Log important events safely.
<?php
class BotLogger {
private string $logFile;
public function __construct(string $logFile) {
$this->logFile = $logFile;
}
public function log(string $event, array $data = []): void {
$log = sprintf(
"[%s] %s | User: %s | Chat: %s\n",
date('Y-m-d H:i:s'),
$event,
$data['user_id'] ?? 'unknown',
$data['chat_id'] ?? 'unknown'
);
error_log($log, 3, $this->logFile);
}
}
$logger = new BotLogger('/var/log/telegram-bot.log');
$logger->log('user_message', [
'user_id' => $bot->update?->message?->from?->id,
'chat_id' => $bot->getChatId(),
]);Always use prepared statements.
<?php
// ❌ WRONG
$result = $db->query("SELECT * FROM users WHERE id = " . $_GET['id']);
// ✅ CORRECT - Prepared statement
$stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
$stmt->bind_param("i", $userId);
$stmt->execute();
$result = $stmt->get_result();Escape output if generating HTML/JSON.
<?php
$userInput = $bot->getTextMessage();
// Safe JSON output
header('Content-Type: application/json');
echo json_encode(['message' => $userInput]);
// Safe HTML output (if needed)
echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');Before going live:
- Token in environment variable
- HTTPS enabled and enforced
- Webhook secret token configured
- Input validation implemented
- Security headers added
- Error logging configured
- .env file in .gitignore
- Debug mode disabled in production
- Rate limiting implemented
- File upload paths validated
- SSL certificate valid and up-to-date
- Regular backups enabled
Report security issues privately. See SECURITY.md for details.