浏览代码

lib/api: Sanitize names used in certificates (fixes #7434) (#7435)

Jakob Borg 4 年之前
父节点
当前提交
df08984a58
共有 2 个文件被更改,包括 61 次插入0 次删除
  1. 40 0
      lib/api/api.go
  2. 21 0
      lib/api/api_test.go

+ 40 - 0
lib/api/api.go

@@ -30,11 +30,15 @@ import (
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 	"time"
 	"time"
+	"unicode"
 
 
 	"github.com/julienschmidt/httprouter"
 	"github.com/julienschmidt/httprouter"
 	metrics "github.com/rcrowley/go-metrics"
 	metrics "github.com/rcrowley/go-metrics"
 	"github.com/thejerf/suture/v4"
 	"github.com/thejerf/suture/v4"
 	"github.com/vitrun/qart/qr"
 	"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/build"
 	"github.com/syncthing/syncthing/lib/config"
 	"github.com/syncthing/syncthing/lib/config"
@@ -153,6 +157,10 @@ func (s *service) getListener(guiCfg config.GUIConfiguration) (net.Listener, err
 		if err != nil {
 		if err != nil {
 			name = s.tlsDefaultCommonName
 			name = s.tlsDefaultCommonName
 		}
 		}
+		name, err = sanitizedHostname(name)
+		if err != nil {
+			name = s.tlsDefaultCommonName
+		}
 
 
 		cert, err = tlsutil.NewCertificate(httpsCertFile, httpsKeyFile, name, httpsCertLifetimeDays)
 		cert, err = tlsutil.NewCertificate(httpsCertFile, httpsKeyFile, name, httpsCertLifetimeDays)
 	}
 	}
@@ -1895,6 +1903,38 @@ func errorString(err error) *string {
 	return nil
 	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 {
 func isFolderNotFound(err error) bool {
 	for _, target := range []error{
 	for _, target := range []error{
 		model.ErrFolderMissing,
 		model.ErrFolderMissing,

+ 21 - 0
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 {
 func equalStrings(a, b []string) bool {
 	if len(a) != len(b) {
 	if len(a) != len(b) {
 		return false
 		return false