Take redirects off the Host header.

Same-site redirects are now relative paths, and a saved page on another site is ignored. The HTTP to HTTPS redirect takes its host from the new SITE_URL setting, and without it the pages refuse plain HTTP.
This commit is contained in:
tim
2026-09-26 15:27:51 +02:00
parent 4657088d23
commit be814a3ba5
16 changed files with 149 additions and 73 deletions
+48 -12
View File
@@ -1,6 +1,7 @@
<?php
namespace Xxx\Tests\Pages;
use PHPUnit\Framework\Attributes\DataProvider;
use Xxx\Tests\Support\PageTestCase;
// publish/login.php, publish/logout.php and persistent logins
@@ -25,16 +26,17 @@ class LoginTest extends PageTestCase {
* sends an editor on to the edit page.
*/
private function assertLoggedIn(array $answer): void {
$this->assertRedirect('https://' . $this->host() . '/publish/login', $answer);
$this->assertRedirect('https://xxx.org.za/publish/edit', $this->get('/publish/login'));
}
private function host(): string {
return parse_url(self::$base, PHP_URL_HOST) . ':' . parse_url(self::$base, PHP_URL_PORT);
$this->assertRedirect('/publish/login', $answer);
$this->assertRedirect('/publish/edit', $this->get('/publish/login'));
}
public function testPlainHttpIsSentToHttps(): void {
$this->assertRedirect('https://xxx.org.za/publish/login', $this->get('/publish/login', array('X-Test-Plain' => '1')));
$this->assertRedirect('https://site.test/publish/login', $this->get('/publish/login', array('X-Test-Plain' => '1')));
}
public function testAForgedHostDoesNotMoveTheHttpsRedirect(): void {
$this->assertRedirect('https://site.test/publish/login',
$this->get('/publish/login', array('X-Test-Plain' => '1', 'Host' => 'evil.example')));
}
public function testTheFormHasATokenAndTheCaptcha(): void {
@@ -60,16 +62,38 @@ class LoginTest extends PageTestCase {
$this->get('/article?id=1');
$answer = $this->post('/publish/login', array('login' => '1', 'username' => 'ed', 'password' => 'secret',
'csrf_token' => $token, 'g-recaptcha-response' => 'pass'));
$this->assertRedirect('https://' . $this->host() . '/article?id=1', $answer);
$this->assertRedirect('/article?id=1', $answer);
$this->assertArrayNotHasKey('referrer', $this->session());
}
public function testAForgedHostDoesNotMoveTheLoginRedirect(): void {
$token = $this->csrfToken($this->get('/publish/login')['body']);
$this->get('/article?id=1');
$answer = $this->request('POST', '/publish/login', array('login' => '1', 'username' => 'ed', 'password' => 'secret',
'csrf_token' => $token, 'g-recaptcha-response' => 'pass'),
array('Host' => 'evil.example'));
$this->assertRedirect('/article?id=1', $answer);
}
public static function otherSites(): array {
return array(array('@evil.example/'), array('//evil.example/'), array('/\\evil.example/'), array('https://evil.example/'));
}
#[DataProvider('otherSites')]
public function testALastPageOnAnotherSiteIsIgnored(string $referrer): void {
$token = $this->csrfToken($this->get('/publish/login')['body']);
$this->startSession(array('csrf_token' => $token, 'referrer' => $referrer));
$answer = $this->post('/publish/login', array('login' => '1', 'username' => 'ed', 'password' => 'secret',
'csrf_token' => $token, 'g-recaptcha-response' => 'pass'));
$this->assertRedirect('/publish/edit', $answer);
}
public function testWithoutALastPageTheLoginGoesToTheEditPage(): void {
$token = $this->csrfToken($this->get('/publish/login')['body']);
$this->startSession(array('csrf_token' => $token));
$answer = $this->post('/publish/login', array('login' => '1', 'username' => 'ed', 'password' => 'secret',
'csrf_token' => $token, 'g-recaptcha-response' => 'pass'));
$this->assertRedirect('https://xxx.org.za/publish/edit', $answer);
$this->assertRedirect('/publish/edit', $answer);
}
public function testTheWrongPasswordIsDenied(): void {
@@ -125,7 +149,7 @@ class LoginTest extends PageTestCase {
public function testAnEditorWhoIsLoggedInGoesToTheEditPage(): void {
$this->loginAs();
$this->assertRedirect('https://xxx.org.za/publish/edit', $this->get('/publish/login'));
$this->assertRedirect('/publish/edit', $this->get('/publish/login'));
}
public function testRememberMeKeepsTheEditorLoggedIn(): void {
@@ -151,7 +175,7 @@ class LoginTest extends PageTestCase {
public function testLogoutNeedsTheToken(): void {
$this->loginAs();
$this->assertRedirect('https://xxx.org.za/', $this->get('/publish/logout?token=wrong'));
$this->assertRedirect('/', $this->get('/publish/logout?token=wrong'));
$this->assertSame('ed', $this->session()['editor']);
}
@@ -164,13 +188,25 @@ class LoginTest extends PageTestCase {
$session = self::$state . '/sessions/sess_' . $this->cookies['PHPSESSID'];
$answer = $this->get('/publish/logout?token=' . $token);
$this->assertRedirect('https://' . $this->host() . '/article?id=2', $answer);
$this->assertRedirect('/article?id=2', $answer);
$this->assertFileDoesNotExist($session);
$this->assertArrayNotHasKey('PHPSESSID', $this->cookies);
$this->assertArrayNotHasKey('PersistentAuth', $this->cookies);
$this->assertSame(array(hash('sha256', 'other device')), array_column($this->tables()['persistent_sessions'], 'session'));
}
public function testAForgedHostDoesNotMoveTheLogoutRedirect(): void {
$token = $this->loginAs('ed', array('referrer' => '/article?id=2'));
$this->assertRedirect('/article?id=2',
$this->get('/publish/logout?token=' . $token, array('Host' => 'evil.example')));
}
#[DataProvider('otherSites')]
public function testLogoutIgnoresALastPageOnAnotherSite(string $referrer): void {
$token = $this->loginAs('ed', array('referrer' => $referrer));
$this->assertRedirect('/', $this->get('/publish/logout?token=' . $token));
}
public function testLogoutDoesNotGoBackToTheLogoutPage(): void {
$token = $this->loginAs('ed', array('referrer' => '/publish/logout'));
$this->assertRedirect('/', $this->get('/publish/logout?token=' . $token));