DomainAliasPatternResolver::resolveAliasPattern() is called by DomainAliasHooks::rewriteRedirectHostname() to compute a redirect target when the active alias is a redirect alias in a non-default environment. It captures wildcards from the live hostname against the source alias pattern and substitutes them positionally into the target alias pattern.
The substitution is positional and context-blind. It works only when the source and target patterns have the same number of * characters in the same intent. When they differ, the resolver silently produces broken URLs.
Mechanism
Two stages:
// 1. Build a regex from the source alias pattern. public static function patternToRegex(string $pattern): string { $escaped = preg_quote($pattern, '/'); $regex = str_replace('*', '(.+?)', $escaped); return '/^' . $regex . '$/i'; } // 2. Walk every "*" in the target pattern, substitute the i-th // capture from the source match. public static function replaceWildcards(string $pattern, array $matches, int $start = 1): string { $index = $start; return preg_replace_callback('/\*/', function () use ($matches, &$index) { return $matches[$index++] ?? '*'; }, $pattern); }
The resolver doesn't know whether a given * represents a subdomain, a TLD, or a port. It just consumes captures left-to-right and substitutes them into the target's * positions.
Failure modes
Case A — same wildcard count (works).
| Role | Pattern |
|---|---|
| Source | *.staging.example.com |
| Target | *-app.staging.example.com |
- Live hostname:
feature-x.staging.example.com - Captures:
['feature-x'] - Substitute capture 1 →
feature-xin the only*of the target. - Result:
feature-x-app.staging.example.com✅
Case B — source has more wildcards than target (silently lossy).
| Role | Pattern |
|---|---|
| Source | *.*.staging.example.com |
| Target | *.canonical.com |
- Live hostname:
feature-x.beta.staging.example.com - Captures:
['feature-x', 'beta'] - Walk target's
*s: only one — capture 1 =feature-x. Capture 2 (beta) is silently dropped. - Result:
feature-x.canonical.com❌ —betavanished. The redirect lands on the wrong hostname; no error, no warning.
Case C — target has more wildcards than source (literal * in URL).
| Role | Pattern |
|---|---|
| Source | *.staging.example.com |
| Target | *.*.canonical.com |
- Live hostname:
feature-x.staging.example.com - Captures:
['feature-x'] - Walk target's
*s: 1st →feature-x; 2nd →$matches[2] ?? '*'falls through to literal'*'. - Result:
feature-x.*.canonical.com❌ — literal*in the redirect URL.TrustedRedirectResponsewill refuse to send to a non-resolvable host, or the user gets a broken page.
Case D — same count but mixed semantics (port slot fed a host capture).
| Role | Pattern |
|---|---|
| Source | *.example.com (1 host wildcard) |
| Target | example.com:* (1 port wildcard) |
- Live hostname:
staging.example.com - Captures:
['staging'] - Walk target's
*s: only one (the:*) →staging. - Result:
example.com:staging❌ —stagingsubstituted as the port. Not a valid port; the URL is broken.
Why it matters
Today (3.x), the validator caps host wildcards at 1, so cases B/C with multiple host wildcards are unreachable through normal admin flows. Sites that imported alias config via direct YAML edit or migrated from D6/D7 can still hit them. Case D is reachable today: nothing in the pipeline prevents a site from defining a host-wildcard source alias and a port-wildcard target alias on the same domain in the same non-default environment.
This issue is the prerequisite for relaxing the host-wildcard cap (separate issue, depends on this one). Raising the cap without hardening the resolver re-introduces all four failure modes at scale.
Proposed fix
- Reject mismatched wildcard counts in
resolveAliasPattern(). Ifsubstr_count(source, '*') !== substr_count(target, '*'), returnFALSErather than producing a broken URL. Drop captures + literal-*-leakage failure modes (cases B and C) silently fail closed instead of silently producing wrong output. - Distinguish host wildcards from port wildcards in the substitution. Split each pattern into host-part and port-part at the first
:. Capture host wildcards from the source-host-part regex, capture the source port (if any) separately. Substitute host-captures into target-host-*s, port-capture into target-port-:*. If counts mismatch within either dimension, returnFALSE. Closes case D. - Add a fail-loud path for developers. When mismatch is detected, log a warning via
$this->loggerFactory->get('domain_alias')so site operators see why their alias-redirect didn't work, instead of silently getting the canonical hostname back.
Test plan
- Add a kernel test for
DomainAliasPatternResolver::resolveAliasPattern()covering cases A, B, C, D explicitly. ExistingDomainAliasPrefixTestexercises the redirect-rewrite path but only with same-count source/target pairs; extend with mismatch fixtures expected to returnFALSE. - Verify the warning log appears for each mismatch case.
- Spot-check the redirect path on a multi-alias setup with a deliberate count mismatch — confirm the redirect falls back to the canonical hostname (current safe-ish behaviour) and a warning is logged.
Out of scope
- Raising the host-wildcard cap to match the README's "max 3" claim. Tracked separately; depends on this issue landing first.
- Restoring D6/D7 single-character
?wildcard support. Tracked separately; the validator is currently in a deprecation cycle for?patterns (#3588169).
Audit context
Surfaced while auditing #3588155, #3588168, and #3588169 on the alias matching pipeline. The resolver fragility is a long-standing latent bug; it became visible once the audit started thinking about why the validator caps host wildcards at 1 in the first place.
Issue fork domain-3588175
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #3
mably commentedComment #5
mably commented