From df08984a5856b85bea7af70832fea016a45cdfb9 Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Thu, 11 Mar 2021 13:15:03 +0100 Subject: [PATCH] lib/api: Sanitize names used in certificates (fixes #7434) (#7435) --- lib/api/api.go | 40 ++++++++++++++++++++++++++++++++++++++++ lib/api/api_test.go | 21 +++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/lib/api/api.go b/lib/api/api.go index ac1778bb1..186adb578 100644 --- a/lib/api/api.go +++ b/lib/api/api.go @@ -30,11 +30,15 @@ import ( "strconv" "strings" "time" + "unicode" "github.com/julienschmidt/httprouter" metrics "github.com/rcrowley/go-metrics" "github.com/thejerf/suture/v4" "github.com/vitrun/qart/qr" + "golang.org/x/text/runes" + "golang.org/x/text/transform" + "golang.org/x/text/unicode/norm" "github.com/syncthing/syncthing/lib/build" "github.com/syncthing/syncthing/lib/config" @@ -153,6 +157,10 @@ func (s *service) getListener(guiCfg config.GUIConfiguration) (net.Listener, err if err != nil { name = s.tlsDefaultCommonName } + name, err = sanitizedHostname(name) + if err != nil { + name = s.tlsDefaultCommonName + } cert, err = tlsutil.NewCertificate(httpsCertFile, httpsKeyFile, name, httpsCertLifetimeDays) } @@ -1895,6 +1903,38 @@ func errorString(err error) *string { return nil } +// sanitizedHostname returns the given name in a suitable form for use as +// the common name in a certificate, or an error. +func sanitizedHostname(name string) (string, error) { + // Remove diacritics and non-alphanumerics. This works by first + // transforming into normalization form D (things with diacriticals are + // split into the base character and the mark) and then removing + // undesired characters. + t := transform.Chain( + // Split runes with diacritics into base character and mark. + norm.NFD, + // Leave only [A-Za-z0-9-.]. + runes.Remove(runes.Predicate(func(r rune) bool { + return r > unicode.MaxASCII || + !unicode.IsLetter(r) && !unicode.IsNumber(r) && + r != '.' && r != '-' + }))) + name, _, err := transform.String(t, name) + if err != nil { + return "", err + } + + // Name should not start or end with a dash or dot. + name = strings.Trim(name, "-.") + + // Name should not be empty. + if name == "" { + return "", errors.New("no suitable name") + } + + return strings.ToLower(name), nil +} + func isFolderNotFound(err error) bool { for _, target := range []error{ model.ErrFolderMissing, diff --git a/lib/api/api_test.go b/lib/api/api_test.go index 39749a5e2..2aab8308f 100644 --- a/lib/api/api_test.go +++ b/lib/api/api_test.go @@ -1370,6 +1370,27 @@ func TestConfigChanges(t *testing.T) { } } +func TestSanitizedHostname(t *testing.T) { + cases := []struct { + in, out string + }{ + {"foo.BAR-baz", "foo.bar-baz"}, + {"~.~-Min 1:a Räksmörgås-dator 😀😎 ~.~-", "min1araksmorgas-dator"}, + {"Vicenç-PC", "vicenc-pc"}, + {"~.~-~.~-", ""}, + {"", ""}, + } + + for _, tc := range cases { + res, err := sanitizedHostname(tc.in) + if tc.out == "" && err == nil { + t.Errorf("%q should cause error", tc.in) + } else if res != tc.out { + t.Errorf("%q => %q, expected %q", tc.in, res, tc.out) + } + } +} + func equalStrings(a, b []string) bool { if len(a) != len(b) { return false