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-x in 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 ❌ — beta vanished. 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. TrustedRedirectResponse will 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 ❌ — staging substituted 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

  1. Reject mismatched wildcard counts in resolveAliasPattern(). If substr_count(source, '*') !== substr_count(target, '*'), return FALSE rather than producing a broken URL. Drop captures + literal-*-leakage failure modes (cases B and C) silently fail closed instead of silently producing wrong output.
  2. 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, return FALSE. Closes case D.
  3. 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. Existing DomainAliasPrefixTest exercises the redirect-rewrite path but only with same-count source/target pairs; extend with mismatch fixtures expected to return FALSE.
  • 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

Command icon 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

mably created an issue. See original summary.

mably’s picture

Assigned: mably » Unassigned
Priority: Major » Normal
Status: Active » Needs review

  • mably committed a09d519a on 3.x
    fix: #3588175 DomainAliasPatternResolver::replaceWildcards() positional...
mably’s picture

Status: Needs review » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.