Skip to content

validate: trim every trailing dot and read Unicode full stops as labels - #282

Merged
TMHSDigital merged 1 commit into
TMHSDigital:mainfrom
bfs2021:fix/normalize-trailing-dots
Nov 14, 2026
Merged

TMHSDigital merged 1 commit into
TMHSDigital:mainfrom
bfs2021:fix/normalize-trailing-dots

Conversation

@bfs2021

@bfs2021 bfs2021 commented Nov 14, 2026

Copy link
Copy Markdown
Contributor

Problem

NormalizeDomain can return a name that still ends in a dot, and it treats Unicode full stops (。, ., 。) as ordinary label characters instead of IDNA label separators. FuzzNormalizeDomain (added for #259) finds both: example.com.. normalizes to example.com., and bücher.example。 comes back as xn--bcher-kva.xn--example-dp3f. Normalising the result again gives a different answer, so two entry points can disagree on the canonical name. Reported in #281.

Root cause

Two separate gaps in internal/validate/validate.go:

  • NormalizeDomain used strings.TrimSuffix(s, "."), which removes one dot at most, and did it before the IDN conversion — so a dot left over from a pasted domain was converted as part of a label, not trimmed.
  • U+3002, U+FF0E and U+FF61 are mapped to . by IDNA (UTS Release linux/amd64 binary is dynamically linked, contradicting the "single static binary" claim #46), but nothing did that mapping, so a trailing ideographic full stop became a label.
  • Domain also trimmed one trailing dot itself, so it accepted the non-canonical form that NormalizeDomain was supposed to have removed.

Fix

  • Map U+3002/U+FF0E/U+FF61 to . while the input is still being read, via a package-level strings.Replacer and a small normalizeFullStops helper.
  • Use strings.TrimRight(s, ".") so every trailing dot is removed.
  • Reject a trailing dot in Domain outright; the only caller that could produce one is normalisation, and normalisation now trims it, so this closes the path instead of hiding it.

Testing

  • Added table cases to TestNormalizeDomain (example.com.., example.com。, bücher.example。, bücher.example.。, example.com) and example.com. to the invalid list for Domain. All of them fail on the parent commit (--- FAIL: TestNormalizeDomain/example.com.., --- FAIL: TestDomain/invalid_example.com., …) and pass with the change.
  • go test ./... — all packages pass.
  • go vet ./internal/validate/ clean, gofmt -l internal/validate/ reports nothing.

Diff: +19/-2 across two files.

Fixes #281

NormalizeDomain removed at most one trailing dot and only after the IDN
conversion, so "example.com.." came back as "example.com." and
"bücher.example。" turned the ideographic full stop into a label of its own.
Domain() then accepted the trailing-dot form, leaving two entry points with
different ideas of the canonical name.

Map U+3002, U+FF0E and U+FF61 to "." before anything else and trim all
trailing dots, then reject a trailing dot in Domain so callers cannot
reintroduce the non-canonical form. Normalizing an accepted domain now
returns the input unchanged, which is what FuzzNormalizeDomain checks.

Fixes TMHSDigital#281
@TMHSDigital
TMHSDigital merged commit 3fe5574 into TMHSDigital:main Nov 14, 2026
16 checks passed
TMHSDigital added a commit that referenced this pull request Nov 14, 2026
#282 fixed the trailing-dot and Unicode full stop handling that
FuzzNormalizeDomain found; credit it in the changelog and stop the fuzz
target's comment saying the bug is still there.

Refs #281

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@TMHSDigital

Copy link
Copy Markdown
Owner

Thanks @bfs2021! Merged as 3fe5574. Before merging I ran the patch locally against the validate, scan and root tests plus 60 seconds of FuzzNormalizeDomain, which used to fail within seconds and now passes. The changelog credits you in fdbd444.

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.

NormalizeDomain keeps a trailing dot and ignores Unicode full stops

2 participants