Skip to content

[Bug] OAuth client creation accepts and stores undocumented scopes #3406

Description

@GG-Feng

Is this a support request?

  • This is not a support request

Is there an existing issue for this?

  • I have searched the existing issues

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

  1. Start headscale in a local container or development environment.

  2. 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"
    
  3. List OAuth clients:

    headscale oauth-clients list
    
  4. Observe that the undocumented scope strings are stored and displayed.

  5. 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"
    
  6. 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

  • Headscale is behind a (reverse) proxy
  • Headscale runs in a container

Debug information

Debug information

Tested commit:

565fd254d06c4c7f9a8cad1714a43445c79ba420

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions