1
0
mirror of https://github.com/juanfont/headscale.git synced 2025-07-27 13:48:02 +02:00

policy: ensure group and tags can only have ascii chars and dash

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
Kristoffer Dalby 2025-05-22 12:32:32 +02:00
parent 27a518a2fa
commit 4d3483ed3a
No known key found for this signature in database
2 changed files with 221 additions and 6 deletions

View File

@ -205,10 +205,14 @@ func (u Username) Resolve(_ *Policy, users types.Users, nodes types.Nodes) (*net
type Group string
func (g Group) Validate() error {
if isGroup(string(g)) {
return nil
if !isGroup(string(g)) {
return fmt.Errorf(`Group has to start with "group:", got: %q`, g)
}
return fmt.Errorf(`Group has to start with "group:", got: %q`, g)
// Group name is everything after "group:"
groupName := string(g)[len("group:"):]
return validateNameFormat(groupName, "Group", g)
}
func (g *Group) UnmarshalJSON(b []byte) error {
@ -266,10 +270,14 @@ func (g Group) Resolve(p *Policy, users types.Users, nodes types.Nodes) (*netipx
type Tag string
func (t Tag) Validate() error {
if isTag(string(t)) {
return nil
if !isTag(string(t)) {
return fmt.Errorf(`tag has to start with "tag:", got: %q`, t)
}
return fmt.Errorf(`tag has to start with "tag:", got: %q`, t)
// Tag name is everything after "tag:"
tagName := string(t)[len("tag:"):]
return validateNameFormat(tagName, "Tag", t)
}
func (t *Tag) UnmarshalJSON(b []byte) error {
@ -654,6 +662,36 @@ func isTag(str string) bool {
return strings.HasPrefix(str, "tag:")
}
// validateNameFormat checks if a name follows the required format:
// - Must start with an ASCII letter (a-z, A-Z)
// - Can only contain ASCII letters, numbers, or dashes
func validateNameFormat(name string, typeLabel string, original interface{}) error {
// Check if empty
if len(name) == 0 {
return fmt.Errorf(`%s names cannot be empty, got: %q`, typeLabel, original)
}
// Check if first character is an ASCII letter
firstChar := name[0]
if !((firstChar >= 'a' && firstChar <= 'z') || (firstChar >= 'A' && firstChar <= 'Z')) {
return fmt.Errorf(`%s names must start with a letter, got: %q`, typeLabel, original)
}
// Check if all characters are ASCII letters, numbers, or dashes
for i := 0; i < len(name); i++ {
char := name[i]
isAsciiLetter := (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z')
isDigit := char >= '0' && char <= '9'
isDash := char == '-'
if !isAsciiLetter && !isDigit && !isDash {
return fmt.Errorf(`%s names can only contain ASCII letters, numbers, or dashes, got: %q`, typeLabel, original)
}
}
return nil
}
func isAutoGroup(str string) bool {
return strings.HasPrefix(str, "autogroup:")
}
@ -1077,6 +1115,39 @@ func (to TagOwners) MarshalJSON() ([]byte, error) {
// TagOwners are a map of Tag to a list of the UserEntities that own the tag.
type TagOwners map[Tag]Owners
// UnmarshalJSON overrides the default JSON unmarshalling for TagOwners to ensure
// that each tag name is validated using the isTag function and character validation rules.
// This ensures that all tag names conform to the expected format and character rules.
func (to *TagOwners) UnmarshalJSON(b []byte) error {
var rawTagOwners map[string][]string
if err := json.Unmarshal(b, &rawTagOwners); err != nil {
return err
}
*to = make(TagOwners)
for key, value := range rawTagOwners {
tag := Tag(key)
if err := tag.Validate(); err != nil {
return err
}
var owners Owners
for _, o := range value {
owner, err := parseOwner(o)
if err != nil {
return err
}
owners = append(owners, owner)
}
(*to)[tag] = owners
}
return nil
}
func (to TagOwners) Contains(tagOwner *Tag) error {
if tagOwner == nil {
return nil

View File

@ -389,6 +389,150 @@ func TestUnmarshalPolicy(t *testing.T) {
// wantErr: `Username has to contain @, got: "group:inner"`,
wantErr: `Nested groups are not allowed, found "group:inner" inside "group:example"`,
},
{
name: "invalid-group-name-special-chars",
input: `
{
"groups": {
"group:example@invalid": [
"valid@example.com",
],
},
}
`,
wantErr: `Group names can only contain ASCII letters, numbers, or dashes, got: "group:example@invalid"`,
},
{
name: "invalid-group-name-starting-with-number",
input: `
{
"groups": {
"group:123example": [
"valid@example.com",
],
},
}
`,
wantErr: `Group names must start with a letter, got: "group:123example"`,
},
{
name: "invalid-group-name-scandinavian-characters",
input: `
{
"groups": {
"group:æøå-example": [
"valid@example.com",
],
},
}
`,
wantErr: `Group names must start with a letter, got: "group:æøå-example"`,
},
{
name: "invalid-group-name-cyrillic-characters",
input: `
{
"groups": {
"group:группа": [
"valid@example.com",
],
},
}
`,
wantErr: `Group names must start with a letter, got: "group:группа"`,
},
{
name: "invalid-group-name-emoji",
input: `
{
"groups": {
"group:dev-😊": [
"valid@example.com",
],
},
}
`,
wantErr: `Group names can only contain ASCII letters, numbers, or dashes, got: "group:dev-😊"`,
},
{
name: "invalid-group-name-other-special-chars",
input: `
{
"groups": {
"group:dev_team": [
"valid@example.com",
],
},
}
`,
wantErr: `Group names can only contain ASCII letters, numbers, or dashes, got: "group:dev_team"`,
},
{
name: "invalid-tag-name-special-chars",
input: `
{
"tagOwners": {
"tag:test@invalid": ["valid@example.com"],
},
}
`,
wantErr: `Tag names can only contain ASCII letters, numbers, or dashes, got: "tag:test@invalid"`,
},
{
name: "invalid-tag-name-starting-with-number",
input: `
{
"tagOwners": {
"tag:123test": ["valid@example.com"],
},
}
`,
wantErr: `Tag names must start with a letter, got: "tag:123test"`,
},
{
name: "invalid-tag-name-scandinavian-characters",
input: `
{
"tagOwners": {
"tag:æøå-test": ["valid@example.com"],
},
}
`,
wantErr: `Tag names must start with a letter, got: "tag:æøå-test"`,
},
{
name: "invalid-tag-name-cyrillic-characters",
input: `
{
"tagOwners": {
"tag:тест": ["valid@example.com"],
},
}
`,
wantErr: `Tag names must start with a letter, got: "tag:тест"`,
},
{
name: "invalid-tag-name-emoji",
input: `
{
"tagOwners": {
"tag:test-😊": ["valid@example.com"],
},
}
`,
wantErr: `Tag names can only contain ASCII letters, numbers, or dashes, got: "tag:test-😊"`,
},
{
name: "invalid-tag-name-other-special-chars",
input: `
{
"tagOwners": {
"tag:test_underscore": ["valid@example.com"],
},
}
`,
wantErr: `Tag names can only contain ASCII letters, numbers, or dashes, got: "tag:test_underscore"`,
},
{
name: "invalid-addr",
input: `