Skip to content

Commit 4b79941

Browse files
authored
Merge pull request #76 from php-telegram-bot/validate-secret_token
Add and validate new secret_token webhook parameter
2 parents 0186409 + 7dd905f commit 4b79941

File tree

4 files changed

+85
-20
lines changed

4 files changed

+85
-20
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Exclamation symbols (:exclamation:) note something of importance e.g. breaking c
77
### Notes
88
- [:ledger: View file changes][Unreleased]
99
### Added
10+
- Enforce `secret_token` webhook validation check.
1011
### Changed
1112
### Deprecated
1213
### Removed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@ $bot = new BotManager([
211211
'max_connections' => 20,
212212
// (array) List the types of updates you want your bot to receive.
213213
'allowed_updates' => ['message', 'edited_channel_post', 'callback_query'],
214+
// (string) Secret token to validate webhook requests.
215+
'secret_token' => 'super_secret_token',
214216
],
215217

216218
// (bool) Only allow webhook access from valid Telegram API IPs.

src/BotManager.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ public function validateAndSetWebhook(): self
206206
'certificate' => $webhook['certificate'] ?? null,
207207
'max_connections' => $webhook['max_connections'] ?? null,
208208
'allowed_updates' => $webhook['allowed_updates'] ?? null,
209+
'secret_token' => $webhook['secret_token'] ?? null,
209210
], function ($v, $k) {
210211
if ($k === 'allowed_updates') {
211212
// Special case for allowed_updates, which can be an empty array.
@@ -562,6 +563,12 @@ public function isValidRequest(): bool
562563
return true;
563564
}
564565

566+
return $this->isValidRequestIp()
567+
&& $this->isValidRequestSecretToken();
568+
}
569+
570+
protected function isValidRequestIp(): bool
571+
{
565572
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
566573
foreach (['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR'] as $key) {
567574
if (filter_var($_SERVER[$key] ?? null, FILTER_VALIDATE_IP)) {
@@ -576,6 +583,18 @@ public function isValidRequest(): bool
576583
));
577584
}
578585

586+
protected function isValidRequestSecretToken(): bool
587+
{
588+
$secret_token = $this->params->getBotParam('webhook.secret_token');
589+
$secret_token_api = $_SERVER['HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN'] ?? null;
590+
591+
if ($secret_token || $secret_token_api) {
592+
return $secret_token === $secret_token_api;
593+
}
594+
595+
return true;
596+
}
597+
579598
/**
580599
* Make sure this is a valid request.
581600
*

tests/BotManagerTest.php

Lines changed: 63 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,26 @@ public static function setUpBeforeClass(): void
4747
];
4848
}
4949

50+
protected function setUp(): void
51+
{
52+
unset($_SERVER['HTTP_X_FORWARDED_FOR'], $_SERVER['HTTP_CLIENT_IP'], $_SERVER['REMOTE_ADDR'], $_SERVER['HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN']);
53+
54+
parent::setUp();
55+
}
56+
5057
/**
5158
* To test the live commands, act as if we're being called by Telegram.
5259
*/
53-
protected function makeRequestValid(): void
60+
protected function makeRequestIpValid(): void
5461
{
5562
$_SERVER['REMOTE_ADDR'] = '149.154.167.197';
5663
}
5764

65+
protected function applyRequestSecretToken(?string $token): void
66+
{
67+
$_SERVER['HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN'] = $token;
68+
}
69+
5870
public function testSetParameters(): void
5971
{
6072
$botManager = new BotManager(array_merge(ParamsTest::$demo_vital_params, [
@@ -233,7 +245,7 @@ public function testValidateAndSetWebhookSuccessLiveBot(): void
233245
*/
234246
public function testDeleteWebhookViaRunLiveBot(): void
235247
{
236-
$this->makeRequestValid();
248+
$this->makeRequestIpValid();
237249
$_GET = ['a' => 'unset'];
238250
$botManager = new BotManager(array_merge(self::$live_params, [
239251
'webhook' => ['url' => 'https://example.com/hook.php'],
@@ -380,35 +392,33 @@ public function testIsValidRequestValidateByDefault(): void
380392
self::assertTrue($botManager->getParams()->getBotParam('validate_request'));
381393
}
382394

383-
public function testIsValidRequestFailValidation(): void
384-
{
385-
$botManager = new BotManager(ParamsTest::$demo_vital_params);
386-
387-
unset($_SERVER['HTTP_X_FORWARDED_FOR'], $_SERVER['HTTP_CLIENT_IP'], $_SERVER['REMOTE_ADDR']);
388-
389-
foreach (['HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'REMOTE_ADDR'] as $key) {
390-
$_SERVER[$key] = '1.1.1.1';
391-
self::assertFalse($botManager->isValidRequest());
392-
unset($_SERVER[$key]);
393-
}
394-
}
395-
396395
public function testIsValidRequestSkipValidation(): void
397396
{
398397
$botManager = new BotManager(array_merge(ParamsTest::$demo_vital_params, [
399398
'validate_request' => false,
399+
'webhook' => ['secret_token' => 'top-secret'],
400400
]));
401401

402-
unset($_SERVER['HTTP_X_FORWARDED_FOR'], $_SERVER['HTTP_CLIENT_IP'], $_SERVER['REMOTE_ADDR']);
402+
$_SERVER['REMOTE_ADDR'] = '1.1.1.1';
403+
$this->applyRequestSecretToken('not-legit');
403404

404405
self::assertTrue($botManager->isValidRequest());
405406
}
406407

407-
public function testIsValidRequestValidate(): void
408+
public function testIsValidRequestIpFailValidation(): void
408409
{
409410
$botManager = new BotManager(ParamsTest::$demo_vital_params);
410411

411-
unset($_SERVER['HTTP_X_FORWARDED_FOR'], $_SERVER['HTTP_CLIENT_IP'], $_SERVER['REMOTE_ADDR']);
412+
foreach (['HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'REMOTE_ADDR'] as $key) {
413+
$_SERVER[$key] = '1.1.1.1';
414+
self::assertFalse($botManager->isValidRequest());
415+
unset($_SERVER[$key]);
416+
}
417+
}
418+
419+
public function testIsValidRequestIpValidate(): void
420+
{
421+
$botManager = new BotManager(ParamsTest::$demo_vital_params);
412422

413423
// Lower range.
414424
$_SERVER['REMOTE_ADDR'] = '149.154.159.255';
@@ -431,13 +441,46 @@ public function testIsValidRequestValidate(): void
431441
self::assertFalse($botManager->isValidRequest());
432442
}
433443

444+
public function testIsValidRequestSecretTokenValidation(): void
445+
{
446+
$this->makeRequestIpValid();
447+
448+
// NO config + NO header = VALID
449+
$botManager = new BotManager(ParamsTest::$demo_vital_params);
450+
self::assertTrue($botManager->isValidRequest());
451+
452+
// NO config + YES header = INVALID
453+
$this->applyRequestSecretToken('top-secret');
454+
self::assertFalse($botManager->isValidRequest());
455+
456+
// YES config + NO header = INVALID
457+
$botManager = new BotManager(array_merge(ParamsTest::$demo_vital_params, [
458+
'webhook' => ['secret_token' => 'top-secret'],
459+
]));
460+
$this->applyRequestSecretToken(null);
461+
self::assertFalse($botManager->isValidRequest());
462+
463+
// YES config + YES header
464+
$botManager = new BotManager(array_merge(ParamsTest::$demo_vital_params, [
465+
'webhook' => ['secret_token' => 'top-secret'],
466+
]));
467+
468+
// + NO equal = INVALID
469+
$this->applyRequestSecretToken('not-legit');
470+
self::assertFalse($botManager->isValidRequest());
471+
472+
// + YES equal = VALID
473+
$this->applyRequestSecretToken('top-secret');
474+
self::assertTrue($botManager->isValidRequest());
475+
}
476+
434477
/**
435478
* @group live
436479
* @runInSeparateProcess
437480
*/
438481
public function testGetUpdatesLiveBot(): void
439482
{
440-
$this->makeRequestValid();
483+
$this->makeRequestIpValid();
441484
$botManager = new BotManager(self::$live_params);
442485
$output = $botManager->run()->getOutput();
443486
self::assertStringContainsString('Updates processed: 0', $output);
@@ -449,7 +492,7 @@ public function testGetUpdatesLiveBot(): void
449492
*/
450493
public function testGetUpdatesLoopLiveBot(): void
451494
{
452-
$this->makeRequestValid();
495+
$this->makeRequestIpValid();
453496
// Webhook MUST NOT be set for this to work!
454497
$this->testDeleteWebhookViaRunLiveBot();
455498

0 commit comments

Comments
 (0)