Skip to content

fix(fqdn): reject domain labels with a trailing hyphen - #1592

Open
mahirhir wants to merge 3 commits into
go-playground:masterfrom
mahirhir:fix-fqdn-trailing-hyphen
Open

fix(fqdn): reject domain labels with a trailing hyphen#1592
mahirhir wants to merge 3 commits into
go-playground:masterfrom
mahirhir:fix-fqdn-trailing-hyphen

Conversation

@mahirhir

Copy link
Copy Markdown

Fixes Or Enhances

The fqdn validator accepts domain labels that end in a hyphen, even though those are not valid hostnames. All of these pass today but shouldn't:

  • foo-.example.com (first label ends in a hyphen)
  • foo.bar-.com (middle label ends in a hyphen)
  • example.com- (TLD ends in a hyphen)

fqdnRegexStringRFC1123 matches each label with [a-zA-Z0-9]{1}[a-zA-Z0-9-]{0,62}, i.e. one alphanumeric followed by up to 62 of [a-zA-Z0-9-], so nothing forces the last character of a label to be alphanumeric.

The hostname validators had the same issue and were fixed in #1565 (RFC 1123) and #1569 (RFC 952), which changed each label to [a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?. The fqdn pattern was not updated at the same time, even though its comment says it is "same as hostnameRegexStringRFC1123 but must contain a non numerical TLD". This PR applies that same label shape to the fqdn pattern, including the TLD (which still has to start with a letter).

Behaviour that stays the same: the non-numeric TLD requirement, the optional trailing dot, and hyphens inside the last label added in #1548 (test-site-http.test-site still validates). I diffed the new pattern against the old one over a large set of random label strings; the only inputs whose result changes are the ones with a label ending in a hyphen.

Added regression cases to TestFQDNValidation and ran the package tests locally.

Make sure that you've checked the boxes below before you submit PR:

  • Tests exist or have been written that cover this particular change.

@go-playground/validator-maintainers

@mahirhir
mahirhir requested a review from a team as a code owner June 28, 2026 20:26
Comment thread validator_test.go
{"24.example24.com", true},
{"test.24.example.com", true},
{"test-site-http.test-site", true},
{"foo-.example.com", false},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps some test cases confirming label length (63 characters) would be helpful as an addition to the test suite? I believe the regex seems correct, but it may be beneficial for future contributors, what do you think?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point!
I agree that having explicit boundary tests will be really helpful for future contributors and prevent regressions.

@mahirhir

Copy link
Copy Markdown
Author

Added cases for a 63-char label (passes, the RFC max a single label can be) and 64 (fails). TestFQDNValidation passes.

fqdnRegexStringRFC1123 matched each label with
[a-zA-Z0-9]{1}[a-zA-Z0-9-]{0,62}, which allows a label to end in a
hyphen (e.g. "foo-.example.com" or "example.com-"). The hostname
validators were fixed for the same issue in go-playground#1565 (RFC 1123) and go-playground#1569
(RFC 952), but the fqdn pattern, whose comment says it should match
hostnameRegexStringRFC1123, was left behind.

Use the same label shape [a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])? so
a label must start and end with an alphanumeric. The non-numeric TLD,
the optional trailing dot, and hyphens inside the last label (go-playground#1548) are
preserved.
@mahirhir
mahirhir force-pushed the fix-fqdn-trailing-hyphen branch from 47c584f to 44963db Compare August 31, 2026 18:44
The existing boundary cases only exercise the first label. The FQDN
pattern matches the first label, the middle labels and the TLD with
three separate groups, so a 63/64 pair on the first one says nothing
about the other two. Adds the same pair at both remaining positions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mahirhir

Copy link
Copy Markdown
Author

@georgepsarakis @nodivbyzero — done in 7dc249f, and you were right in a way I want to spell out, because my first reaction was "already covered".

The branch already had a 63/64 pair, but only on the first label:

{strings.Repeat("a", 63) + ".com", true},
{strings.Repeat("a", 64) + ".com", false},

The pattern matches a hostname with three separate groups: the first label, the middle labels, and the TLD. Each carries its own {0,61}. So a boundary test on the first group is not evidence about the other two, and if someone edited one of those quantifiers the suite would have stayed green.

Measured against the pattern before writing anything, so the expectations are observations rather than guesses:

  first label 63     matches=true       first label 64     matches=false
  middle label 63    matches=true       middle label 64    matches=false
  tld 63             matches=true       tld 64             matches=false
  example.com        matches=true                                   (control)

Added the missing four:

{"foo." + strings.Repeat("a", 63) + ".com", true},
{"foo." + strings.Repeat("a", 64) + ".com", false},
{"foo." + strings.Repeat("a", 63), true},
{"foo." + strings.Repeat("a", 64), false},

And checked that they constrain something, by widening the middle-label quantifier from {0,61} to {0,99} and re-running:

validator_test.go:11333: Index: 21 fqdn failed Error: <nil>
FAIL

Index 21 is the new 64-character middle-label case. Before this commit that mutation passed the suite.

go test -run TestFQDNValidation ./...   ok   github.com/go-playground/validator/v10

On the red lint check: it is not from this branch. Every finding is exhaustruct_v5 in benchmarks_test.go, which this PR does not touch:

benchmarks_test.go:192:74: validator.valuer is missing field Name (exhaustruct_v5)
benchmarks_test.go:288:71: validator.TestStruct is missing field String (exhaustruct_v5)
benchmarks_test.go:434:11: validator.Test is missing field NickName (exhaustruct_v5)
...

The workflow pins version: latest for golangci-lint, so a newer release enabling or tightening that linter turns the job red on every PR regardless of content. Happy to leave that alone, or to open a separate PR filling those struct literals in if you would like it fixed, but it seemed wrong to bundle it here.

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.

3 participants