Skip to content

fix(server): match access rule patterns against the whole value - #7018

Merged
otavio merged 1 commit into
masterfrom
fix/anchor-access-rule-patterns
Sep 2, 2026
Merged

fix(server): match access rule patterns against the whole value#7018
otavio merged 1 commit into
masterfrom
fix/anchor-access-rule-patterns

Conversation

@otavio

@otavio otavio commented Sep 2, 2026

Copy link
Copy Markdown
Member

The public-key device filter, the public-key username restriction and the Access Policy device
filter all selected with regexp.MatchString(pattern, value), which succeeds on any substring. A
filter written as staging reached notstaging, staging-db and prod-staging-mirror; a key
restricted to root also authorized the login notroot.

These fields are regexps and are documented as regexps, so this was the semantics they carried
rather than a break in them. It is still the wrong default for a field that decides access: the
operator types a hostname and gets a substring rule, and the surprise runs in the permissive
direction. Within a single policy the inconsistency is visible — logins: ["ubuntu"] refused
ubuntu2 while filter.hostname: "staging" accepted notstaging.

What changes

models.MatchPattern wraps the pattern as \A(?:pattern)\z and every matcher goes through it:

  • PublicKeyFilter.Matches — the device selector shared by the public-key ACL and Access Policies
  • EvaluateKeyUsername — the public-key username restriction

The non-capturing group is load-bearing: anchoring a|b without it yields \Aa|b\z, which means
something else. \A/\z rather than ^/$ so a (?m) pattern cannot pass one line of a
multiline value off as a whole match.

An empty pattern still matches anything, and .* — the value the console stores for "all devices"
— still matches every name.

Breaking change

This narrows every stored pattern that relied on substring reach. A filter written as staging now
selects only the device named staging; an operator who depended on the wider reach needs
staging.* or a tag filter. It wants a release note.

Notes

Cloud carries the same matcher in its firewall rule evaluator; shellhub-io/cloud#2530 moves those
three call sites onto MatchPattern and is blocked on this landing.

Documentation and console copy are updated separately, in the companion PR, so the wording change
can be read on its own.

Reported by Eduardo Barbosa (@Edu0x01).

The public-key device filter, the public-key username restriction and the
Access Policy device filter all selected with regexp.MatchString(pattern,
value), which succeeds on any substring. An administrator who writes
"staging" into a device filter is naming the staging host; what the server
enforced was every device whose name contains "staging", including
notstaging, staging-db and prod-staging-mirror. A key restricted to "root"
also authorized the login "notroot".

The fields are regexps and are documented as regexps, so this was the
semantics they carried rather than a break in them. It is still the wrong
default for a field that decides access: the operator types a hostname and
gets a substring rule, and the surprise runs in the permissive direction.

Within one rule the mismatch shows. subjectMatches and loginMatches compare
with ==, sourceIPMatches uses netip.Prefix.Contains, so logins:["ubuntu"]
refused "ubuntu2" while filter.hostname:"staging" accepted "notstaging" -
two selectors on the same policy reading their values by different rules.

MatchPattern wraps the pattern as \A(?:pattern)\z. The non-capturing group
is load-bearing: anchoring "a|b" without it yields \Aa|b\z, which means
something else entirely. \A and \z rather than ^ and $ so that a (?m)
pattern cannot pass off one line of a multiline value as a whole match.

This narrows every stored pattern that relied on substring matching. A
filter written as "staging" now selects only the device named "staging";
an operator who depended on the wider reach needs "staging.*" or a tag
filter. That is a breaking change for such deployments and wants a release
note.

Reported-by: Eduardo Barbosa <Edu0x01@users.noreply.github.com>
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

Code Review Complete

The automated review ran but did not post an updated summary — this usually means no new issues were found since the previous review. If you've pushed changes and want a fresh pass, comment /review.

View job

@otavio
otavio merged commit b9f5105 into master Sep 2, 2026
41 checks passed
@otavio
otavio deleted the fix/anchor-access-rule-patterns branch September 2, 2026 18:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant