Skip to content
Open
Changes from 1 commit
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
01be4ba
feat(network): let public address validators accept an allowlist
abnegate Oct 1, 2026
576790f
fix(migrations): validate source endpoints
abnegate Oct 1, 2026
ff22697
refactor(network): move subnet and allowlist validators into utopia-p…
abnegate Oct 1, 2026
70a567a
test(migrations): cover stored endpoint revalidation end to end
abnegate Oct 1, 2026
ca568c6
fix: harden medium-severity auth, installer, and proxy defaults
cursoragent Oct 1, 2026
23a7825
fix: restore local openssl key and Traefik proxy CIDRs for CI
cursoragent Oct 1, 2026
c07a250
Merge remote-tracking branch 'origin/main' into cursor/security-mediu…
abnegate Oct 2, 2026
d5e1523
test(realtime): wait for the matching event when deleting an attribute
abnegate Oct 2, 2026
0af7731
fix(network): trust private proxies when _APP_TRUSTED_PROXIES is unset
abnegate Oct 2, 2026
40526d6
test(account): cover concurrent recovery resets with one token
abnegate Oct 2, 2026
e93a8fb
fix(account): reject recovery when the token was already consumed
abnegate Oct 2, 2026
b90ff5f
fix(install): keep the existing encryption key on upgrade
abnegate Oct 2, 2026
720eed3
fix(auth): warn instead of refusing to boot on the placeholder key
abnegate Oct 2, 2026
3d2ce3b
fix(install): print installer secret from parent when server stdout i…
cursoragent Oct 2, 2026
9862422
refactor(install): move installer secret and key resolution into doma…
cursoragent Oct 2, 2026
04eba75
test(account): cover session and MFA challenge invalidation on passwo…
cursoragent Oct 2, 2026
217bb21
test(realtime): wait for the matching event when deleting an index
cursoragent Oct 2, 2026
f00f017
test(account): prove session revocation instead of relying on a 401
cursoragent Oct 2, 2026
9201b04
fix(migrations): pin appwrite source and report requests to the valid…
abnegate Oct 2, 2026
5fbde29
fix(network): keep blocking lookups in coroutines without network hooks
abnegate Oct 2, 2026
b07a1b8
test(migrations): cover an endpoint admitted by an allowed subnet end…
abnegate Oct 2, 2026
b0eafd9
Merge branch 'main' into fix/migration-endpoint-ssrf
abnegate Oct 2, 2026
294dfd3
test(migrations): check resolve entries with the Subnet validator
abnegate Oct 2, 2026
0c4cb9a
test(migrations): reach the allowed subnet through a platform hostname
abnegate Oct 2, 2026
7719413
fix(account): reject a reused recovery token outside the transaction
cursoragent Oct 2, 2026
acfcde7
refactor(user): delete pending MFA challenges in one bulk delete
cursoragent Oct 2, 2026
594b65f
fix(projects): report session invalidation as on when unset for 1.x c…
cursoragent Oct 2, 2026
ecb0425
fix(install): require the installer secret for status and certificate…
cursoragent Oct 2, 2026
21a30cd
fix(install): print the installer secret before waiting for the server
cursoragent Oct 2, 2026
68c2948
refactor(install): let prepareEnvironmentVariables decide the encrypt…
cursoragent Oct 2, 2026
0d6776a
perf(request): read trusted proxies once per request
cursoragent Oct 2, 2026
c0011bb
chore: date _APP_TRUSTED_PROXIES to 2.3.0 and trim explanatory docblocks
cursoragent Oct 2, 2026
5469f23
revert(env): keep your-secret-key as the local development key
cursoragent Oct 2, 2026
e65a378
fix(request): resolve client IP as the rightmost untrusted forwarded hop
cursoragent Oct 2, 2026
93aefad
feat(network): trust RFC 6598 by default and drop unused TrustedProxi…
cursoragent Oct 2, 2026
3774c91
test(users): cover session and MFA challenge invalidation on server p…
cursoragent Oct 2, 2026
57fc7f1
Merge pull request #14048 from appwrite/cursor/security-mediums-2026-…
abnegate Oct 2, 2026
113f875
feat(avatars): store the placeholder as the photo on deletePhoto
Meldiron Oct 2, 2026
2aa55a4
Merge pull request #14065 from appwrite/feat-delete-photo-fallback
Meldiron Oct 2, 2026
965d7e8
fix(security): block private/reserved hosts on OAuth, messaging and m…
eldadfux Sep 29, 2026
3eea704
fix(security): make the destination policy part of the HTTP client (S…
loks0n Oct 1, 2026
b9c7ce9
refactor(client): rename Destination to Destinations and default to A…
loks0n Oct 1, 2026
0f19ebd
test: build clients over the real adapter where no request is sent
loks0n Oct 2, 2026
ddaa97e
fix(client): share the connect timeout between DNS and the Swoole socket
loks0n Oct 2, 2026
cf4deae
Merge pull request #13998 from appwrite/fix-ssrf-fetch-sinks
loks0n Oct 2, 2026
ee1a647
fix(client): unrestricted Swoole clients leave name resolution to Swoole
loks0n Oct 2, 2026
ec5290a
fix(client): dial a bare IPv6 address when leaving resolution to Swoole
loks0n Oct 2, 2026
83701a9
fix(client): reach a hostname that has only an IPv6 address on Swoole
loks0n Oct 2, 2026
473f504
fix(client): the IPv6 retry shares the first attempt's connect timeout
loks0n Oct 2, 2026
326adfb
fix(client): send the prepared request on the Swoole IPv6 retry
loks0n Oct 2, 2026
35563f5
test(client): read Content-Length with string functions in the resolv…
loks0n Oct 2, 2026
35a6757
Merge pull request #14066 from appwrite/fix-swoole-anywhere-resolution
loks0n Oct 2, 2026
b72632a
fix(migrations): check appwrite source endpoints against the destinat…
abnegate Oct 2, 2026
4d77768
chore(migrations): rebuild on the destination policy from main
abnegate Oct 2, 2026
94b04c1
fix(client): treat ipv4-compatible and site-local ipv6 as reserved
abnegate Oct 2, 2026
07358a9
fix(migrations): accept _APP_ALLOWED_INTERNAL_ADDRESSES for appwrite …
abnegate Oct 2, 2026
e3ba3f3
fix(migrations): keep hostnames out of _APP_ALLOWED_INTERNAL_ADDRESSES
abnegate Oct 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
test(migrations): cover stored endpoint revalidation end to end
Worker behaviour is covered by e2e tests, not worker unit tests. The create
route now refuses private endpoints, so the e2e test stores an Appwrite
migration directly, the way a job created before validation would look,
retries it through the API and checks that the real worker records the
endpoint failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
  • Loading branch information
abnegate and claude committed Oct 1, 2026
commit 70a567a8ffbcbb376b92be4eb519e35cbe89395a
82 changes: 82 additions & 0 deletions tests/e2e/Services/Migrations/MigrationsBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,27 @@

namespace Tests\E2E\Services\Migrations;

use Appwrite\Database\Factory;
use Appwrite\Tests\Retry;
use CURLFile;
use PHPUnit\Framework\Attributes\Depends;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Services\Functions\FunctionsBase;
use Tests\E2E\Services\Realtime\RealtimeBase;
use Utopia\Cache\Adapter\Pool as CachePool;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
use Utopia\Command;
use Utopia\Config\Config;
use Utopia\Console;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Migration\Resource;
use Utopia\Migration\Sources\Appwrite;
use WebSocket\ConnectionException;
Expand Down Expand Up @@ -311,6 +317,82 @@ public function testAppwriteMigrationRejectsPrivateEndpoints(): void
}
}

public function testRetryAppwriteMigrationWithStoredPrivateEndpoint(): void
{
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
];

foreach (['http://169.254.169.254/v1', 'http://[::1]/v1'] as $endpoint) {
$migrationId = ID::unique();

$this->createMigrationFixture($this->getDestinationProject()['$id'], new Document([
'$id' => $migrationId,
'status' => 'failed',
'stage' => 'finished',
'source' => Appwrite::getName(),
'destination' => Appwrite::getName(),
'credentials' => [
'endpoint' => $endpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
],
'resources' => [Resource::TYPE_USER],
'statusCounters' => '{}',
'resourceData' => '{}',
'errors' => [],
'options' => [],
]));

$retry = $this->client->call(Client::METHOD_PATCH, '/migrations/' . $migrationId, $headers);
$this->assertSame(204, $retry['headers']['status-code'], "Retry refused for {$endpoint}");

$migration = [];
$this->assertEventually(function () use ($migrationId, &$migration) {
$migration = $this->getMigrationStatus($migrationId);

$this->assertNotSame([], $migration['errors']);
}, 60_000, 1_000);

$this->assertSame('failed', $migration['status'], $endpoint);
$this->assertSame('finished', $migration['stage'], $endpoint);
$this->assertStringContainsString('Invalid `endpoint`', \implode(',', $migration['errors']), $endpoint);
}
}

/**
* Stores a migration the way one created before endpoint validation was
* introduced would be stored, bypassing the create route that now refuses it.
*/
private function createMigrationFixture(string $projectId, Document $migration): void
{
$seed = function () use ($projectId, $migration): void {
global $register;
$pools = $register->get('pools');
$cache = new Cache(new Sharding(\array_map(
fn (string $name) => new CachePool($pools->get($name)),
Config::getParam('pools-cache', []),
)));
$authorization = new Authorization();
$factory = new Factory($pools, $cache, $authorization);

$authorization->skip(function () use ($factory, $projectId, $migration): void {
$project = $factory->platform()->getDocument('projects', $projectId);
$this->assertFalse($project->isEmpty(), "Project {$projectId} not found");

$factory->project($project)->createDocument('migrations', $migration);
});
};

if (\Swoole\Coroutine::getCid() >= 0) {
$seed();
} else {
\Swoole\Coroutine\run($seed);
}
}

/**
* Auth
*/
Expand Down