mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-12 02:02:15 +01:00
Compare commits
1 Commits
main
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
466897e11f |
4
.github/workflows/build-onpremise.yml
vendored
4
.github/workflows/build-onpremise.yml
vendored
@@ -141,7 +141,7 @@ jobs:
|
|||||||
${{ env.DOCKER_REPO }}
|
${{ env.DOCKER_REPO }}
|
||||||
|
|
||||||
- name: "Login to solidtime OnPremise Registry"
|
- name: "Login to solidtime OnPremise Registry"
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@v4.5.2
|
||||||
with:
|
with:
|
||||||
registry: registry.on-premise.solidtime.io
|
registry: registry.on-premise.solidtime.io
|
||||||
username: ${{ secrets.ONPREMISE_USERNAME }}
|
username: ${{ secrets.ONPREMISE_USERNAME }}
|
||||||
@@ -195,7 +195,7 @@ jobs:
|
|||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: "Login to solidtime OnPremise Registry"
|
- name: "Login to solidtime OnPremise Registry"
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@v4.5.2
|
||||||
with:
|
with:
|
||||||
registry: registry.on-premise.solidtime.io
|
registry: registry.on-premise.solidtime.io
|
||||||
username: ${{ secrets.ONPREMISE_USERNAME }}
|
username: ${{ secrets.ONPREMISE_USERNAME }}
|
||||||
|
|||||||
2
.github/workflows/build-private.yml
vendored
2
.github/workflows/build-private.yml
vendored
@@ -177,7 +177,7 @@ jobs:
|
|||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
|
||||||
- name: "Login to GitHub Container Registry"
|
- name: "Login to GitHub Container Registry"
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@v4.5.2
|
||||||
with:
|
with:
|
||||||
registry: rg.fr-par.scw.cloud/solidtime
|
registry: rg.fr-par.scw.cloud/solidtime
|
||||||
username: nologin
|
username: nologin
|
||||||
|
|||||||
8
.github/workflows/build-public.yml
vendored
8
.github/workflows/build-public.yml
vendored
@@ -117,13 +117,13 @@ jobs:
|
|||||||
${{ env.GHCR_REPO }}
|
${{ env.GHCR_REPO }}
|
||||||
|
|
||||||
- name: "Login to Docker Hub Container Registry"
|
- name: "Login to Docker Hub Container Registry"
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@v4.5.2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: "Login to GitHub Container Registry"
|
- name: "Login to GitHub Container Registry"
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@v4.5.2
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -177,13 +177,13 @@ jobs:
|
|||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: "Login to Docker Hub"
|
- name: "Login to Docker Hub"
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@v4.5.2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: "Login to GHCR"
|
- name: "Login to GHCR"
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@v4.5.2
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ namespace App\Exceptions;
|
|||||||
|
|
||||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
class Handler extends ExceptionHandler
|
class Handler extends ExceptionHandler
|
||||||
@@ -33,29 +30,6 @@ class Handler extends ExceptionHandler
|
|||||||
$this->reportable(function (Throwable $e): void {
|
$this->reportable(function (Throwable $e): void {
|
||||||
//
|
//
|
||||||
});
|
});
|
||||||
|
|
||||||
// A request on an untrusted host (see App\Http\Middleware\TrustHosts)
|
|
||||||
// otherwise renders as a bare "Bad request." 400. Show a message that
|
|
||||||
// says how to fix it instead. The framework has already converted the
|
|
||||||
// SuspiciousOperationException into a BadRequestHttpException by the time
|
|
||||||
// renderables run, so we match that and inspect the original.
|
|
||||||
$this->renderable(function (BadRequestHttpException $e, Request $request): ?Response {
|
|
||||||
$previous = $e->getPrevious();
|
|
||||||
|
|
||||||
if (! $previous instanceof SuspiciousOperationException
|
|
||||||
|| ! str_starts_with($previous->getMessage(), 'Untrusted Host')) {
|
|
||||||
return null; // any other bad request keeps the default response
|
|
||||||
}
|
|
||||||
|
|
||||||
$message = 'This hostname is not configured for this instance. '
|
|
||||||
.'Set APP_URL, or add the host to TRUSTED_HOSTS.';
|
|
||||||
|
|
||||||
if ($request->expectsJson()) {
|
|
||||||
return response()->json(['message' => $message], 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->view('errors.untrusted-host', ['message' => $message], 400);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render($request, Throwable $e): Response|RedirectResponse
|
public function render($request, Throwable $e): Response|RedirectResponse
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ use App\Http\Middleware\PreventRequestsDuringMaintenance;
|
|||||||
use App\Http\Middleware\RedirectIfAuthenticated;
|
use App\Http\Middleware\RedirectIfAuthenticated;
|
||||||
use App\Http\Middleware\ShareInertiaData;
|
use App\Http\Middleware\ShareInertiaData;
|
||||||
use App\Http\Middleware\TrimStrings;
|
use App\Http\Middleware\TrimStrings;
|
||||||
use App\Http\Middleware\TrustHosts;
|
|
||||||
use App\Http\Middleware\TrustProxies;
|
use App\Http\Middleware\TrustProxies;
|
||||||
use App\Http\Middleware\ValidateSignature;
|
use App\Http\Middleware\ValidateSignature;
|
||||||
use App\Http\Middleware\VerifyCsrfToken;
|
use App\Http\Middleware\VerifyCsrfToken;
|
||||||
@@ -48,7 +47,6 @@ class Kernel extends HttpKernel
|
|||||||
*/
|
*/
|
||||||
protected $middleware = [
|
protected $middleware = [
|
||||||
ForceHttps::class,
|
ForceHttps::class,
|
||||||
TrustHosts::class,
|
|
||||||
TrustProxies::class,
|
TrustProxies::class,
|
||||||
HandleCors::class,
|
HandleCors::class,
|
||||||
PreventRequestsDuringMaintenance::class,
|
PreventRequestsDuringMaintenance::class,
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
|
||||||
|
|
||||||
use Illuminate\Http\Middleware\TrustHosts as BaseTrustHosts;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Http\Response;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rejects requests whose Host is not trusted, preventing Host-header poisoning of
|
|
||||||
* generated URLs (password reset, SSO callback, invitations). Trusted = the
|
|
||||||
* APP_URL host and its subdomains, plus TRUSTED_HOSTS (for multi-host access such
|
|
||||||
* as a Tailscale name). Health-check endpoints are exempt (probed by IP).
|
|
||||||
*/
|
|
||||||
class TrustHosts extends BaseTrustHosts
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @return array<int, string|null>
|
|
||||||
*/
|
|
||||||
public function hosts(): array
|
|
||||||
{
|
|
||||||
/** @var array<int, string> $configured */
|
|
||||||
$configured = config('app.trusted_hosts', []);
|
|
||||||
|
|
||||||
$extra = array_map(function (string $host): string {
|
|
||||||
$host = trim($host);
|
|
||||||
|
|
||||||
// "*.example.com" matches any subdomain, not the apex.
|
|
||||||
if (str_starts_with($host, '*.')) {
|
|
||||||
return '^.+\.'.preg_quote(substr($host, 2), '#').'$';
|
|
||||||
}
|
|
||||||
|
|
||||||
return '^'.preg_quote($host, '#').'$';
|
|
||||||
}, $configured);
|
|
||||||
|
|
||||||
return array_merge([$this->allSubdomainsOfApplicationUrl()], $extra);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param \Closure(Request): Response $next
|
|
||||||
*/
|
|
||||||
public function handle(Request $request, $next)
|
|
||||||
{
|
|
||||||
// Exempt health checks (probed by IP). Also reset the trusted hosts,
|
|
||||||
// since Octane leaks the static state across requests.
|
|
||||||
if ($request->is('health-check/*')) {
|
|
||||||
Request::setTrustedHosts([]);
|
|
||||||
|
|
||||||
return $next($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
return parent::handle($request, $next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -75,27 +75,6 @@ return [
|
|||||||
|
|
||||||
'url' => env('APP_URL', 'http://localhost'),
|
'url' => env('APP_URL', 'http://localhost'),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Trusted Hosts
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Additional hostnames (besides the APP_URL host and its subdomains) that
|
|
||||||
| the application is allowed to respond on. This is needed for multi-host
|
|
||||||
| setups, e.g. reaching the instance over both a public domain and a
|
|
||||||
| Tailscale name. A request arriving on any host that is neither APP_URL
|
|
||||||
| (nor a subdomain of it) nor listed here is rejected, which prevents
|
|
||||||
| Host-header poisoning of password reset and other out-of-band links.
|
|
||||||
|
|
|
||||||
| See App\Http\Middleware\TrustHosts.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'trusted_hosts' => array_values(array_filter(array_map(
|
|
||||||
'trim',
|
|
||||||
explode(',', (string) env('TRUSTED_HOSTS', ''))
|
|
||||||
))),
|
|
||||||
|
|
||||||
'asset_url' => env('ASSET_URL'),
|
'asset_url' => env('ASSET_URL'),
|
||||||
|
|
||||||
'force_https' => (bool) env('APP_FORCE_HTTPS', false),
|
'force_https' => (bool) env('APP_FORCE_HTTPS', false),
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
{{-- Self-contained on purpose: this page is rendered for a request on an
|
|
||||||
untrusted host, so it must not call url()/route()/asset(), which would
|
|
||||||
re-trigger Host validation and throw again. --}}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>Untrusted host</title>
|
|
||||||
<style>
|
|
||||||
html, body { height: 100%; margin: 0; }
|
|
||||||
body {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: #f5f5f5;
|
|
||||||
color: #1f2937;
|
|
||||||
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
||||||
}
|
|
||||||
.card {
|
|
||||||
max-width: 32rem;
|
|
||||||
margin: 1.5rem;
|
|
||||||
padding: 2rem;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
h1 { margin: 0 0 0.75rem; font-size: 1.25rem; }
|
|
||||||
p { margin: 0; line-height: 1.6; color: #4b5563; }
|
|
||||||
code {
|
|
||||||
padding: 0.1rem 0.35rem;
|
|
||||||
background: #f3f4f6;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="card">
|
|
||||||
<h1>Untrusted host</h1>
|
|
||||||
<p>
|
|
||||||
This hostname is not configured for this instance. Set
|
|
||||||
<code>APP_URL</code>, or add the host to <code>TRUSTED_HOSTS</code>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,271 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Unit\Middleware;
|
|
||||||
|
|
||||||
use App\Http\Middleware\TrustHosts;
|
|
||||||
use Illuminate\Contracts\Debug\ExceptionHandler;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
|
||||||
use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
#[CoversClass(TrustHosts::class)]
|
|
||||||
class TrustHostsTest extends TestCase
|
|
||||||
{
|
|
||||||
private const string CANONICAL = 'https://app.example.com';
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
|
|
||||||
config(['app.url' => self::CANONICAL]);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function tearDown(): void
|
|
||||||
{
|
|
||||||
Request::setTrustedHosts([]); // don't leak static state between tests
|
|
||||||
parent::tearDown();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The real middleware, with only the environment gate forced on (it
|
|
||||||
* self-exempts in the testing environment).
|
|
||||||
*/
|
|
||||||
private function middleware(): TrustHosts
|
|
||||||
{
|
|
||||||
return new class($this->app) extends TrustHosts
|
|
||||||
{
|
|
||||||
protected function shouldSpecifyTrustedHosts(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private function accepts(Request $request): bool
|
|
||||||
{
|
|
||||||
$this->middleware()->handle($request, fn (Request $request): Response => new Response('passed'));
|
|
||||||
|
|
||||||
try {
|
|
||||||
dump($request->getHost());
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (\Throwable) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_canonical_host_is_accepted(): void
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
$request = Request::create(self::CANONICAL.'/login');
|
|
||||||
|
|
||||||
// Act
|
|
||||||
$accepted = $this->accepts($request);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$this->assertTrue($accepted);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_subdomain_of_canonical_host_is_accepted(): void
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
$request = Request::create('https://team.app.example.com/login');
|
|
||||||
|
|
||||||
// Act
|
|
||||||
$accepted = $this->accepts($request);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$this->assertTrue($accepted);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_declared_trusted_host_is_accepted(): void
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
config(['app.trusted_hosts' => ['box.tailnet.ts.net']]);
|
|
||||||
$request = Request::create('https://box.tailnet.ts.net/login');
|
|
||||||
|
|
||||||
// Act
|
|
||||||
$accepted = $this->accepts($request);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$this->assertTrue($accepted);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_wildcard_trusted_host_matches_subdomains_only(): void
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
config(['app.trusted_hosts' => ['*.example.net']]);
|
|
||||||
$subdomainRequest = Request::create('https://foo.example.net/login');
|
|
||||||
$nestedSubdomainRequest = Request::create('https://a.b.example.net/login');
|
|
||||||
$apexRequest = Request::create('https://example.net/login');
|
|
||||||
$suffixInjectionRequest = Request::create('https://example.net.evil.com/login');
|
|
||||||
|
|
||||||
// Act
|
|
||||||
$subdomainAccepted = $this->accepts($subdomainRequest);
|
|
||||||
$nestedSubdomainAccepted = $this->accepts($nestedSubdomainRequest);
|
|
||||||
$apexAccepted = $this->accepts($apexRequest);
|
|
||||||
$suffixInjectionAccepted = $this->accepts($suffixInjectionRequest);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$this->assertTrue($subdomainAccepted);
|
|
||||||
$this->assertTrue($nestedSubdomainAccepted);
|
|
||||||
$this->assertFalse($apexAccepted);
|
|
||||||
$this->assertFalse($suffixInjectionAccepted);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_multiple_trusted_hosts_are_all_accepted(): void
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
config(['app.trusted_hosts' => [
|
|
||||||
'box.tailnet.ts.net',
|
|
||||||
'solidtime.internal',
|
|
||||||
'*.preview.example.com',
|
|
||||||
]]);
|
|
||||||
$tailnetRequest = Request::create('https://box.tailnet.ts.net/login');
|
|
||||||
$internalRequest = Request::create('https://solidtime.internal/login');
|
|
||||||
$previewRequest = Request::create('https://pr-42.preview.example.com/login');
|
|
||||||
$unlistedRequest = Request::create('https://evil.example.com/login');
|
|
||||||
|
|
||||||
// Act
|
|
||||||
$tailnetAccepted = $this->accepts($tailnetRequest);
|
|
||||||
$internalAccepted = $this->accepts($internalRequest);
|
|
||||||
$previewAccepted = $this->accepts($previewRequest);
|
|
||||||
$unlistedAccepted = $this->accepts($unlistedRequest);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$this->assertTrue($tailnetAccepted);
|
|
||||||
$this->assertTrue($internalAccepted);
|
|
||||||
$this->assertTrue($previewAccepted);
|
|
||||||
$this->assertFalse($unlistedAccepted);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_poisoned_host_is_rejected(): void
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
$request = Request::create('https://evil.example.com/login');
|
|
||||||
|
|
||||||
// Act
|
|
||||||
$accepted = $this->accepts($request);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$this->assertFalse($accepted);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_poisoned_x_forwarded_host_is_rejected(): void
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
$request = Request::create(self::CANONICAL.'/login');
|
|
||||||
$request->headers->set('X-Forwarded-Host', 'evil.example.com');
|
|
||||||
$request->setTrustedProxies(
|
|
||||||
['0.0.0.0/0', '2000::/3'],
|
|
||||||
Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST |
|
|
||||||
Request::HEADER_X_FORWARDED_PROTO | Request::HEADER_X_FORWARDED_PORT
|
|
||||||
);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
$accepted = $this->accepts($request);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$this->assertFalse($accepted);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_forwarded_host_from_trusted_proxy_is_accepted(): void
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
$request = Request::create('https://evil.example.com/login');
|
|
||||||
$request->headers->set('X-Forwarded-Host', 'app.example.com');
|
|
||||||
$request->setTrustedProxies(
|
|
||||||
['0.0.0.0/0', '2000::/3'],
|
|
||||||
Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST |
|
|
||||||
Request::HEADER_X_FORWARDED_PROTO | Request::HEADER_X_FORWARDED_PORT
|
|
||||||
);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
$accepted = $this->accepts($request);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$this->assertTrue($accepted);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_forwarded_host_from_non_trusted_proxy_is_rejected_if_host_is_allowed(): void
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
$request = Request::create('https://evil.example.com/login');
|
|
||||||
$request->headers->set('X-Forwarded-Host', 'app.example.com');
|
|
||||||
$request->setTrustedProxies(
|
|
||||||
['1.2.3.4/32'], // Not a trusted proxy
|
|
||||||
Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST |
|
|
||||||
Request::HEADER_X_FORWARDED_PROTO | Request::HEADER_X_FORWARDED_PORT
|
|
||||||
);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
$accepted = $this->accepts($request);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$this->assertFalse($accepted);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_health_check_endpoint_bypasses_host_validation(): void
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
$internalIpRequest = Request::create('https://0.0.0.0/health-check/up');
|
|
||||||
$localhostRequest = Request::create('http://localhost/health-check/up');
|
|
||||||
|
|
||||||
// Act
|
|
||||||
$internalIpAccepted = $this->accepts($internalIpRequest);
|
|
||||||
$localhostAccepted = $this->accepts($localhostRequest);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$this->assertTrue($internalIpAccepted);
|
|
||||||
$this->assertTrue($localhostAccepted);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_health_check_endpoint_clears_state_before_other_middleware_reads_the_host(): void
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
Request::setTrustedHosts(['^app\.example\.com$']);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
$response = $this->get(self::CANONICAL.'/health-check/up', ['Host' => '0.0.0.0']);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$response->assertSuccessful()
|
|
||||||
->assertExactJson(['success' => true]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_untrusted_host_renders_a_helpful_error(): void
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
$handler = app(ExceptionHandler::class);
|
|
||||||
$exception = new SuspiciousOperationException('Untrusted Host "evil.example.com".');
|
|
||||||
$request = Request::create('https://evil.example.com/login');
|
|
||||||
|
|
||||||
// Act
|
|
||||||
$response = $handler->render($request, $exception);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$this->assertSame(400, $response->getStatusCode());
|
|
||||||
$this->assertStringContainsString('TRUSTED_HOSTS', (string) $response->getContent());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_untrusted_host_returns_json_for_api_clients(): void
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
$handler = app(ExceptionHandler::class);
|
|
||||||
$exception = new SuspiciousOperationException('Untrusted Host "evil.example.com".');
|
|
||||||
$request = Request::create('https://evil.example.com/api/v1/users');
|
|
||||||
$request->headers->set('Accept', 'application/json');
|
|
||||||
|
|
||||||
// Act
|
|
||||||
$response = $handler->render($request, $exception);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
$this->assertSame(400, $response->getStatusCode());
|
|
||||||
$this->assertJson((string) $response->getContent());
|
|
||||||
$this->assertStringContainsString('TRUSTED_HOSTS', (string) $response->getContent());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user