Is this a support request?
Is there an existing issue for this?
Current Behavior
Current Behavior
OAuth client creation accepts scope strings that are not part of the documented scope vocabulary and stores them verbatim.
The CLI help lists the expected scope values:
auth_keys, oauth_keys, devices:core, devices:routes, policy_file, feature_settings, read variants, all, all:read
However, the create path only requires at least one scope and does not validate the submitted values against that vocabulary.
Relevant code in cmd/headscale/cli/oauth_client.go:
if len(scopes) == 0 {
return fmt.Errorf("at least one --scope is required: %w", errMissingParameter)
}
resp, err := client.CreateKeyWithResponse(ctx, oauthTailnet, clientv2.CreateKeyRequest{
KeyType: &keyType,
Scopes: &scopes,
Tags: &tags,
Description: &description,
})
The database creation path validates tags with validateACLTags, but scopes are only deduplicated and sorted before being stored.
Relevant code in hscontrol/db/oauth.go:
tags, err := validateACLTags(tags)
if err != nil {
return "", nil, err
}
scopes = set.SetOf(scopes).Slice()
slices.Sort(scopes)
client := types.OAuthClient{
Scopes: scopes,
Tags: tags,
Description: description,
UserID: creatorUserID,
CreatedAt: &now,
}
Local validation output:
#### STEP 1: malicious/arbitrary scopes + VALID tag (expect SUCCESS) ####
OAuth client b54801670ee2 created.
#### STEP 2: list -> confirm bogus scopes persisted verbatim ####
ID | Scopes | Tags | Description | Created
b54801670ee2 | *::*,../../etc/passwd,EVIL:superuser | tag:legit | repro-unvalidated-scopes | 2026-07-30 04:43:58
#### STEP 3: CONTRAST — valid scope + INVALID tag 'notagprefix' (expect FAIL) ####
Error: api error (400): creating oauth client
This shows that tags are rejected when malformed, while undocumented scope strings are accepted and stored.
Notes:
hscontrol/scope.Parse keeps unknown scope strings as-is, and the code comments state that unknown strings do not satisfy required scopes. So this report is about input validation and stored OAuth client configuration consistency, not a demonstrated privilege escalation.
Suggested fix:
Add scope validation next to validateACLTags in CreateOAuthClient. The validator should deduplicate, sort, and reject any scope that is not in hscontrol/scope.Known().
Expected Behavior
Expected Behavior
OAuth client creation should reject scope values that are not part of the known/documented scope vocabulary.
If the intended behavior is to allow forward-compatible unknown scopes, that behavior should be explicit in the CLI help and API documentation.
Steps To Reproduce
Steps To Reproduce
-
Start headscale in a local container or development environment.
-
Create an OAuth client with undocumented scope strings and a valid tag:
headscale oauth-clients create --scope "EVIL:superuser" --scope "*::*" --scope "../../etc/passwd" --tag "tag:legit" -d "repro-unvalidated-scopes"
-
List OAuth clients:
headscale oauth-clients list
-
Observe that the undocumented scope strings are stored and displayed.
-
As a contrast, create an OAuth client with a valid scope but an invalid tag:
headscale oauth-clients create --scope "all" --tag "notagprefix" -d "repro-bad-tag"
-
Observe that the invalid tag is rejected.
Environment
### Environment
- OS: Linux / Docker
- Headscale version: tested at commit 565fd254d06c4c7f9a8cad1714a43445c79ba420
- Tailscale version: not applicable
- Runtime environment: Headscale runs in a container
Runtime environment
Debug information
Debug information
Tested commit:
565fd254d06c4c7f9a8cad1714a43445c79ba420
Is this a support request?
Is there an existing issue for this?
Current Behavior
Current Behavior
OAuth client creation accepts scope strings that are not part of the documented scope vocabulary and stores them verbatim.
The CLI help lists the expected scope values:
However, the create path only requires at least one scope and does not validate the submitted values against that vocabulary.
Relevant code in cmd/headscale/cli/oauth_client.go:
The database creation path validates tags with validateACLTags, but scopes are only deduplicated and sorted before being stored.
Relevant code in hscontrol/db/oauth.go:
Local validation output:
This shows that tags are rejected when malformed, while undocumented scope strings are accepted and stored.
Notes:
Suggested fix:
Expected Behavior
Expected Behavior
OAuth client creation should reject scope values that are not part of the known/documented scope vocabulary.
If the intended behavior is to allow forward-compatible unknown scopes, that behavior should be explicit in the CLI help and API documentation.
Steps To Reproduce
Steps To Reproduce
Start headscale in a local container or development environment.
Create an OAuth client with undocumented scope strings and a valid tag:
List OAuth clients:
Observe that the undocumented scope strings are stored and displayed.
As a contrast, create an OAuth client with a valid scope but an invalid tag:
Observe that the invalid tag is rejected.
Environment
Runtime environment
Debug information
Debug information
Tested commit: