fix: address security review findings
This commit is contained in:
@@ -8,6 +8,7 @@ test('cleanup deletes only expired login urls matching local mail-login state',
|
||||
'timestamp' => 1782810000,
|
||||
'expires' => 1782810030,
|
||||
'correlation' => 'autologin-h4-mxtest-1782810000',
|
||||
'id' => 'HASHURLMATCH',
|
||||
'redirect_path' => '/',
|
||||
'client_ip' => '198.51.100.10',
|
||||
'ip_bound' => 1,
|
||||
@@ -31,5 +32,17 @@ test('cleanup deletes only expired login urls matching local mail-login state',
|
||||
assert_true(mailLoginCleanupShouldDelete($matching, [$state], strtotime('2026-06-30T10:01:00Z')));
|
||||
assert_false(mailLoginCleanupShouldDelete($wrongRedirect, [$state], strtotime('2026-06-30T10:01:00Z')));
|
||||
assert_false(mailLoginCleanupShouldDelete($notExpired, [$state], strtotime('2026-06-30T10:01:00Z')));
|
||||
|
||||
$stateWithoutId = $state;
|
||||
unset($stateWithoutId['id']);
|
||||
assert_false(mailLoginCleanupShouldDelete($matching, [$stateWithoutId], strtotime('2026-06-30T10:01:00Z')));
|
||||
});
|
||||
|
||||
test('cleanup rejects non-https directadmin api base url', function (): void {
|
||||
try {
|
||||
mailLoginCleanupValidateBaseUrl('http://127.0.0.1:2222');
|
||||
throw new RuntimeException('Expected http base URL to fail');
|
||||
} catch (InvalidArgumentException $e) {
|
||||
assert_contains('https', strtolower($e->getMessage()));
|
||||
}
|
||||
});
|
||||
|
||||
+42
-1
@@ -12,6 +12,7 @@ test('directadmin context derives server name from first host label', function (
|
||||
$ctx = DirectAdminContext::fromServer([
|
||||
'USERNAME' => 'mxtest',
|
||||
'HTTP_HOST' => 'https://h4.domena.pl:2222',
|
||||
'SERVER_NAME' => 'h4.domena.pl',
|
||||
'REMOTE_ADDR' => '198.51.100.10',
|
||||
'REQUEST_METHOD' => 'GET',
|
||||
], $settings);
|
||||
@@ -21,6 +22,34 @@ test('directadmin context derives server name from first host label', function (
|
||||
assert_same('h4', $ctx->serverName());
|
||||
});
|
||||
|
||||
test('directadmin context prefers trusted configured source server name over host header', function (): void {
|
||||
$settings = Settings::fromArray(['MAIL_LOGIN_SOURCE_SERVER_NAME' => 'h4']);
|
||||
$ctx = DirectAdminContext::fromServer([
|
||||
'USERNAME' => 'mxtest',
|
||||
'HTTP_HOST' => 'evil.example.net:2222',
|
||||
'SERVER_NAME' => 'h4.domena.pl',
|
||||
'REMOTE_ADDR' => '198.51.100.10',
|
||||
'REQUEST_METHOD' => 'GET',
|
||||
], $settings);
|
||||
|
||||
assert_same('h4.domena.pl', $ctx->sourceHost());
|
||||
assert_same('h4', $ctx->serverName());
|
||||
});
|
||||
|
||||
test('directadmin context fails closed without directadmin username', function (): void {
|
||||
try {
|
||||
DirectAdminContext::fromServer([
|
||||
'USER' => 'root',
|
||||
'HTTP_HOST' => 'h4.domena.pl:2222',
|
||||
'REMOTE_ADDR' => '198.51.100.10',
|
||||
'REQUEST_METHOD' => 'GET',
|
||||
], Settings::fromArray([]));
|
||||
throw new RuntimeException('Expected missing USERNAME to fail');
|
||||
} catch (InvalidArgumentException $e) {
|
||||
assert_contains('username', strtolower($e->getMessage()));
|
||||
}
|
||||
});
|
||||
|
||||
test('directadmin context rejects invalid usernames', function (): void {
|
||||
try {
|
||||
DirectAdminContext::fromServer([
|
||||
@@ -58,6 +87,19 @@ test('client ip honors forwarded header only from trusted proxy', function (): v
|
||||
assert_same('203.0.113.20', $ip);
|
||||
});
|
||||
|
||||
test('client ip supports trusted proxy cidr ranges', function (): void {
|
||||
$settings = Settings::fromArray([
|
||||
'MAIL_LOGIN_CLIENT_IP_MODE' => 'trusted_proxy',
|
||||
'MAIL_LOGIN_TRUSTED_PROXIES' => '10.0.0.0/24',
|
||||
]);
|
||||
$ip = ClientIp::resolve([
|
||||
'REMOTE_ADDR' => '10.0.0.25',
|
||||
'HTTP_X_FORWARDED_FOR' => '203.0.113.20',
|
||||
], $settings);
|
||||
|
||||
assert_same('203.0.113.20', $ip);
|
||||
});
|
||||
|
||||
test('source authorizer rejects hosts outside allowlist', function (): void {
|
||||
$settings = Settings::fromArray(['MAIL_LOGIN_ALLOWED_SOURCE_HOSTS' => 'h4.domena.pl']);
|
||||
|
||||
@@ -79,4 +121,3 @@ test('key name uses requested format and truncates long values deterministically
|
||||
assert_true(str_starts_with($long, 'autologin-'));
|
||||
assert_true((bool)preg_match('/-[a-f0-9]{10}-1782810000$/', $long), 'Long key should contain hash suffix');
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ test('mx helper conditionally passes user ip binding to da login-url', function
|
||||
escapeshellarg($state),
|
||||
escapeshellarg('h4.domena.pl'),
|
||||
escapeshellarg($script),
|
||||
'mail-login-create-url mxtest h4.domena.pl h4 30 1 198.51.100.10 /'
|
||||
'mail-login-create-url mxtest h4.domena.pl h4 30 1 198.51.100.10 / 1782810000'
|
||||
);
|
||||
exec($cmd, $out, $code);
|
||||
assert_same(0, $code, implode("\n", $out));
|
||||
@@ -33,7 +33,7 @@ test('mx helper conditionally passes user ip binding to da login-url', function
|
||||
escapeshellarg($state),
|
||||
escapeshellarg('h4.domena.pl'),
|
||||
escapeshellarg($script),
|
||||
'mail-login-create-url mxtest h4.domena.pl h4 30 0 198.51.100.10 /'
|
||||
'mail-login-create-url mxtest h4.domena.pl h4 30 0 198.51.100.10 / 1782810001'
|
||||
);
|
||||
exec($cmdNoIp, $outNoIp, $codeNoIp);
|
||||
assert_same(0, $codeNoIp, implode("\n", $outNoIp));
|
||||
@@ -57,7 +57,7 @@ test('mx helper accepts forced command arguments from ssh original command', fun
|
||||
escapeshellarg($audit),
|
||||
escapeshellarg($state),
|
||||
escapeshellarg('h4.domena.pl'),
|
||||
escapeshellarg('mail-login-create-url mxtest h4.domena.pl evil 30 1 198.51.100.10 /'),
|
||||
escapeshellarg('mail-login-create-url mxtest h4.domena.pl evil 30 1 198.51.100.10 / 1782810002'),
|
||||
escapeshellarg($script)
|
||||
);
|
||||
exec($cmd, $out, $code);
|
||||
@@ -83,7 +83,7 @@ test('mx helper rejects disallowed source hosts and invalid client ips', functio
|
||||
escapeshellarg($state),
|
||||
escapeshellarg('h4.domena.pl'),
|
||||
escapeshellarg($script),
|
||||
'mail-login-create-url mxtest evil.domena.pl evil 30 1 198.51.100.10 /'
|
||||
'mail-login-create-url mxtest evil.domena.pl evil 30 1 198.51.100.10 / 1782810003'
|
||||
);
|
||||
exec($badHost, $outHost, $codeHost);
|
||||
assert_same(1, $codeHost);
|
||||
@@ -95,8 +95,9 @@ test('mx helper rejects disallowed source hosts and invalid client ips', functio
|
||||
escapeshellarg($state),
|
||||
escapeshellarg('h4.domena.pl'),
|
||||
escapeshellarg($script),
|
||||
'mail-login-create-url mxtest h4.domena.pl h4 30 1 999.999.999.999 /'
|
||||
'mail-login-create-url mxtest h4.domena.pl h4 30 1 999.999.999.999 / 1782810004'
|
||||
);
|
||||
exec($badIp, $outIp, $codeIp);
|
||||
assert_same(1, $codeIp);
|
||||
assert_not_contains("\nBROKEN", file_get_contents($audit) ?: '');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once PLUGIN_ROOT . '/exec/lib/RateLimiter.php';
|
||||
|
||||
test('rate limiter enforces limit across sequential requests', function (): void {
|
||||
$limiter = new RateLimiter(TEST_TMP . '/rate-limit');
|
||||
|
||||
$limiter->assertAllowed('user-mxtest', 2, 60);
|
||||
$limiter->assertAllowed('user-mxtest', 2, 60);
|
||||
|
||||
try {
|
||||
$limiter->assertAllowed('user-mxtest', 2, 60);
|
||||
throw new RuntimeException('Expected third request to be rate limited');
|
||||
} catch (RuntimeException $e) {
|
||||
assert_contains('rate limit', strtolower($e->getMessage()));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -15,20 +15,22 @@ test('remote login url client builds fixed ssh command with positional arguments
|
||||
'LOGIN_KEY_TTL' => '30',
|
||||
'USER_IP_BOUND' => '1',
|
||||
]);
|
||||
$request = new LoginUrlRequest('mxtest', 'h4.domena.pl', 'h4', 30, true, '198.51.100.10', '/');
|
||||
$request = new LoginUrlRequest('mxtest', 'h4.domena.pl', 'h4', 30, true, '198.51.100.10', '/', 1782810000);
|
||||
$command = RemoteLoginUrlClient::buildCommand($settings, $request);
|
||||
|
||||
assert_same('ssh', $command[0]);
|
||||
assert_contains('UserKnownHostsFile=/secure/known_hosts', implode(' ', $command));
|
||||
assert_contains('root@mx1.domena.pl', implode(' ', $command));
|
||||
assert_same('mail-login-create-url', $command[count($command) - 8]);
|
||||
assert_same('mxtest', $command[count($command) - 7]);
|
||||
assert_same('1', $command[count($command) - 3]);
|
||||
$pos = array_search('mail-login-create-url', $command, true);
|
||||
assert_true(is_int($pos), 'Command marker missing');
|
||||
assert_same('mxtest', $command[$pos + 1]);
|
||||
assert_same('1', $command[$pos + 5]);
|
||||
assert_same('1782810000', $command[$pos + 8]);
|
||||
});
|
||||
|
||||
test('remote login url client validates helper output before returning it', function (): void {
|
||||
$settings = Settings::fromArray(['MAIL_LOGIN_TARGET_BASE_URL' => 'https://mx1.domena.pl:2222']);
|
||||
$request = new LoginUrlRequest('mxtest', 'h4.domena.pl', 'h4', 30, true, '198.51.100.10', '/');
|
||||
$request = new LoginUrlRequest('mxtest', 'h4.domena.pl', 'h4', 30, true, '198.51.100.10', '/', 1782810000);
|
||||
|
||||
$client = new RemoteLoginUrlClient(function (array $command, int $timeout): ProcessResult {
|
||||
return new ProcessResult(0, "URL: https://mx1.domena.pl:2222/api/login/url?key=SECRET\n", '');
|
||||
|
||||
Reference in New Issue
Block a user