Per Greptile P0: loopback as an unconditional "local" host is exploitable.
An attacker submitting endpoint=http://localhost/v1 with a victim's
projectId would bypass the host-validation gate and hit the DB fast path
against the victim's DSN. Dev clusters that genuinely use localhost as the
public domain already pass the host check via the Hostname validator
matching _APP_DOMAIN=localhost — no special-case needed.
A worker container can never legitimately fetch source data from its own
loopback, but the address still means "this same cluster" to whoever wrote
it (internal automation, dev/single-node setups, console pasting localhost
in OSS dev). Treat it as local so the DB fast path is used and downstream
SDK fallbacks within the migration get the worker-host rewrite they need.
Unconditional rewrite on the SDK path could silently redirect a
misrouted external migration to this cluster's own internal API — if
the caller had a valid apiKey for any project on this cluster, the
migration would read from us instead of the typed external source.
Restore the original DB-only gate.
Mirrors src/Appwrite/Network/Cors.php, which already uses Hostname for
CORS allow-listing. Replaces the inline exact + suffix-with-dot logic
with the validator's built-in exact + wildcard handling (*.domain).
Normalizing _APP_DOMAIN through parse_url too closes the same
port-suffix gap that the previous commit closed for _APP_MIGRATION_HOST.
parse_url(PHP_URL_HOST) strips the port from the source URL, so an env
value like 'appwrite:8080' would never equal the parsed source host
'appwrite'. Normalize the env value through the same parse_url so the
local-fast-path routing still triggers when the deployment publishes
the migration host with an explicit port.
Three parity changes with appwrite-labs/cloud#4103:
1. Empty endpoint clause in $isLocalEndpoint so callers bypassing
processMigration's defaulting (e.g. unit tests) still get the DB path
when _APP_MIGRATION_HOST is configured.
2. resolveLocalEndpoint + getMigrationEndpoint helpers, with the inline
construction in processMigration consolidated to use them.
3. Always rewrite loopback source URLs (localhost / 127.0.0.1 / etc.)
to the internal host for SourceAppwrite, on both DB and SDK paths.
Loopback addresses are unreachable from inside the worker container;
the rewrite preserves legit dev scenarios. SDK auth still gates access.
_APP_DOMAIN is the public host users paste from console, but programmatic
migrations that leave `endpoint` blank default to _APP_MIGRATION_HOST in
processMigration. Both are valid references to this cluster; checking only
_APP_DOMAIN rejected the second pattern (e.g. CI tests pass the internal
host directly) and routed legitimate intra-cluster migrations to SDK.
Substring matching on the full URL hit false positives in three ways:
the domain in a query string, a `customer.com.attacker.io` suffix attack,
and `fake-customer.com` superstring matches. Parse the host out of the
URL and match against `_APP_DOMAIN` exactly or as a `.`-prefixed suffix
(to allow region subdomains like `fra.localhost`).
The apiKey-ownership approach required reading project keys and was specific to
the bug case. The simpler endpoint check covers the same bug — same projectId
on source and destination only means "local" when the source endpoint actually
contains this installation's public domain.
The SMTP test email uses email-base-styled.tpl as its base template,
which contains {{platform}}, {{logoUrl}}, {{accentColor}}, and social/
legal link placeholders. These were never passed as template variables,
causing them to render as literal strings (e.g. "{{platform}} logo").
Inject the platform config and pass the variables to MailMessage,
matching the pattern used by OTP and magic-url email flows.
Co-Authored-By: Harsh Mahajan <harsh@appwrite.io>
These fields were already persisted on update but omitted from the response
model, causing them to disappear after a page refresh in the console.
Co-Authored-By: Harsh Mahajan <harsh@appwrite.io>
Drops the redundant Route::resolveParams() call. The matched path
params are now provided directly via the new frame-local 'params'
injection from utopia-php/http, avoiding a second URL parse.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The 'route' injection is frame-local and non-nullable inside a
matched action's hooks. Replaces \$utopia->match() lookups in api.php
and auth.php init hooks, drops the dead \$utopia inject from the
session shutdown hook, and removes the now-redundant null guards.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The 'route' injection introduced in utopia-php/http feat-safe-wildcards
is frame-local and non-nullable inside a matched action's hooks, so
the shutdown handlers in api.php and mock.php no longer need to call
match() and dereference a nullable result.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The documentsdb/vectorsdb routes are registered with
setHttpPath('/v1/documentsdb/...') with no aliases, so getPath()
returns a template containing the substring we're matching against
— and matches the prior getMatchedPath() semantics without depending
on the raw request URI.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>