From e77c7e16b6700cdaddef3a0d8b16579173505436 Mon Sep 17 00:00:00 2001 From: kim <89579420+NyaaaWhatsUpDoc@users.noreply.github.com> Date: Tue, 14 Jan 2025 14:23:18 +0000 Subject: [chore] better dns validation (#3644) * add seperate PunifyValidate() function for properly validating domain names when converting to punycode * rename function, strip port from domain validation --- internal/util/domain.go | 138 +++++++++++++++++++++++++++++++++++++++++++ internal/util/domain_test.go | 101 +++++++++++++++++++++++++++++++ internal/util/puny_test.go | 101 ------------------------------- internal/util/punycode.go | 97 ------------------------------ 4 files changed, 239 insertions(+), 198 deletions(-) create mode 100644 internal/util/domain.go create mode 100644 internal/util/domain_test.go delete mode 100644 internal/util/puny_test.go delete mode 100644 internal/util/punycode.go (limited to 'internal/util') diff --git a/internal/util/domain.go b/internal/util/domain.go new file mode 100644 index 000000000..c64cc65d7 --- /dev/null +++ b/internal/util/domain.go @@ -0,0 +1,138 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package util + +import ( + "net/url" + "strings" + + "golang.org/x/net/idna" +) + +var ( + // IDNA (Internationalized Domain Names for Applications) + // profiles for fast punycode conv and full verification. + punifyProfile = *idna.Punycode + verifyProfile = *idna.Lookup +) + +// PunifySafely validates the provided domain name, +// and converts unicode chars to ASCII, i.e. punified form. +func PunifySafely(domain string) (string, error) { + if i := strings.LastIndexByte(domain, ':'); i >= 0 { + + // If there is a port included in domain, we + // strip it as colon is invalid in a hostname. + domain, port := domain[:i], domain[i:] + domain, err := verifyProfile.ToASCII(domain) + if err != nil { + return "", err + } + + // Then rebuild with port after. + domain = strings.ToLower(domain) + return domain + port, nil + } else { //nolint:revive + + // Otherwise we just punify domain as-is. + domain, err := verifyProfile.ToASCII(domain) + return strings.ToLower(domain), err + } +} + +// Punify is a faster form of PunifySafely() without validation. +func Punify(domain string) (string, error) { + domain, err := punifyProfile.ToASCII(domain) + return strings.ToLower(domain), err +} + +// DePunify converts any punycode-encoded unicode characters +// in domain name back to their origin unicode. Please note +// that this performs minimal validation of domain name. +func DePunify(domain string) (string, error) { + domain = strings.ToLower(domain) + return punifyProfile.ToUnicode(domain) +} + +// URIMatches returns true if the expected URI matches +// any of the given URIs, taking account of punycode. +func URIMatches(expect *url.URL, uris ...*url.URL) (ok bool, err error) { + + // Create new URL to hold + // punified URI information. + punyURI := new(url.URL) + *punyURI = *expect + + // Set punified expected URL host. + punyURI.Host, err = Punify(expect.Host) + if err != nil { + return false, err + } + + // Calculate expected URI string. + expectStr := punyURI.String() + + // Use punyURI to iteratively + // store each punified URI info + // and generate punified URI + // strings to check against. + for _, uri := range uris { + *punyURI = *uri + punyURI.Host, err = Punify(uri.Host) + if err != nil { + return false, err + } + + // Check for a match against expect. + if expectStr == punyURI.String() { + return true, nil + } + } + + // Didn't match. + return false, nil +} + +// PunifyURI returns a new copy of URI with the 'host' +// part converted to punycode with PunifySafely(). +// For simple comparisons prefer the faster URIMatches(). +func PunifyURI(in *url.URL) (*url.URL, error) { + punyHost, err := PunifySafely(in.Host) + if err != nil { + return nil, err + } + out := new(url.URL) + *out = *in + out.Host = punyHost + return out, nil +} + +// PunifyURIToStr returns given URI serialized with the +// 'host' part converted to punycode with PunifySafely(). +// For simple comparisons prefer the faster URIMatches(). +func PunifyURIToStr(in *url.URL) (string, error) { + punyHost, err := PunifySafely(in.Host) + if err != nil { + return "", err + } + oldHost := in.Host + in.Host = punyHost + str := in.String() + in.Host = oldHost + return str, nil +} diff --git a/internal/util/domain_test.go b/internal/util/domain_test.go new file mode 100644 index 000000000..a0ffd30b4 --- /dev/null +++ b/internal/util/domain_test.go @@ -0,0 +1,101 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package util_test + +import ( + "net/url" + "strconv" + "testing" + + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/testrig" +) + +type PunyTestSuite struct { + suite.Suite +} + +func (suite *PunyTestSuite) TestMatches() { + for i, testCase := range []struct { + expect *url.URL + actual []*url.URL + match bool + }{ + { + expect: testrig.URLMustParse("https://%D5%A9%D5%B8%D6%82%D5%A9.%D5%B0%D5%A1%D5%B5/@ankap"), + actual: []*url.URL{ + testrig.URLMustParse("https://xn--69aa8bzb.xn--y9a3aq/users/ankap"), + testrig.URLMustParse("https://xn--69aa8bzb.xn--y9a3aq/@ankap"), + }, + match: true, + }, + { + expect: testrig.URLMustParse("https://xn--69aa8bzb.xn--y9a3aq/@ankap"), + actual: []*url.URL{ + testrig.URLMustParse("https://xn--69aa8bzb.xn--y9a3aq/users/ankap"), + testrig.URLMustParse("https://xn--69aa8bzb.xn--y9a3aq/@ankap"), + }, + match: true, + }, + { + expect: testrig.URLMustParse("https://թութ.հայ/@ankap"), + actual: []*url.URL{ + testrig.URLMustParse("https://xn--69aa8bzb.xn--y9a3aq/users/ankap"), + testrig.URLMustParse("https://xn--69aa8bzb.xn--y9a3aq/@ankap"), + }, + match: true, + }, + { + expect: testrig.URLMustParse("https://թութ.հայ/@ankap"), + actual: []*url.URL{ + testrig.URLMustParse("https://example.org/users/ankap"), + testrig.URLMustParse("https://%D5%A9%D5%B8%D6%82%D5%A9.%D5%B0%D5%A1%D5%B5/@ankap"), + }, + match: true, + }, + { + expect: testrig.URLMustParse("https://example.org/@ankap"), + actual: []*url.URL{ + testrig.URLMustParse("https://xn--69aa8bzb.xn--y9a3aq/users/ankap"), + testrig.URLMustParse("https://xn--69aa8bzb.xn--y9a3aq/@ankap"), + }, + match: false, + }, + } { + matches, err := util.URIMatches( + testCase.expect, + testCase.actual..., + ) + if err != nil { + suite.FailNow(err.Error()) + } + + if matches != testCase.match { + suite.Failf( + "case "+strconv.Itoa(i)+" matches not equal expected", + "wanted %t, got %t", + testCase.match, matches, + ) + } + } +} + +func TestPunyTestSuite(t *testing.T) { + suite.Run(t, new(PunyTestSuite)) +} diff --git a/internal/util/puny_test.go b/internal/util/puny_test.go deleted file mode 100644 index a0ffd30b4..000000000 --- a/internal/util/puny_test.go +++ /dev/null @@ -1,101 +0,0 @@ -// GoToSocial -// Copyright (C) GoToSocial Authors admin@gotosocial.org -// SPDX-License-Identifier: AGPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package util_test - -import ( - "net/url" - "strconv" - "testing" - - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/util" - "github.com/superseriousbusiness/gotosocial/testrig" -) - -type PunyTestSuite struct { - suite.Suite -} - -func (suite *PunyTestSuite) TestMatches() { - for i, testCase := range []struct { - expect *url.URL - actual []*url.URL - match bool - }{ - { - expect: testrig.URLMustParse("https://%D5%A9%D5%B8%D6%82%D5%A9.%D5%B0%D5%A1%D5%B5/@ankap"), - actual: []*url.URL{ - testrig.URLMustParse("https://xn--69aa8bzb.xn--y9a3aq/users/ankap"), - testrig.URLMustParse("https://xn--69aa8bzb.xn--y9a3aq/@ankap"), - }, - match: true, - }, - { - expect: testrig.URLMustParse("https://xn--69aa8bzb.xn--y9a3aq/@ankap"), - actual: []*url.URL{ - testrig.URLMustParse("https://xn--69aa8bzb.xn--y9a3aq/users/ankap"), - testrig.URLMustParse("https://xn--69aa8bzb.xn--y9a3aq/@ankap"), - }, - match: true, - }, - { - expect: testrig.URLMustParse("https://թութ.հայ/@ankap"), - actual: []*url.URL{ - testrig.URLMustParse("https://xn--69aa8bzb.xn--y9a3aq/users/ankap"), - testrig.URLMustParse("https://xn--69aa8bzb.xn--y9a3aq/@ankap"), - }, - match: true, - }, - { - expect: testrig.URLMustParse("https://թութ.հայ/@ankap"), - actual: []*url.URL{ - testrig.URLMustParse("https://example.org/users/ankap"), - testrig.URLMustParse("https://%D5%A9%D5%B8%D6%82%D5%A9.%D5%B0%D5%A1%D5%B5/@ankap"), - }, - match: true, - }, - { - expect: testrig.URLMustParse("https://example.org/@ankap"), - actual: []*url.URL{ - testrig.URLMustParse("https://xn--69aa8bzb.xn--y9a3aq/users/ankap"), - testrig.URLMustParse("https://xn--69aa8bzb.xn--y9a3aq/@ankap"), - }, - match: false, - }, - } { - matches, err := util.URIMatches( - testCase.expect, - testCase.actual..., - ) - if err != nil { - suite.FailNow(err.Error()) - } - - if matches != testCase.match { - suite.Failf( - "case "+strconv.Itoa(i)+" matches not equal expected", - "wanted %t, got %t", - testCase.match, matches, - ) - } - } -} - -func TestPunyTestSuite(t *testing.T) { - suite.Run(t, new(PunyTestSuite)) -} diff --git a/internal/util/punycode.go b/internal/util/punycode.go deleted file mode 100644 index 3e9f71408..000000000 --- a/internal/util/punycode.go +++ /dev/null @@ -1,97 +0,0 @@ -// GoToSocial -// Copyright (C) GoToSocial Authors admin@gotosocial.org -// SPDX-License-Identifier: AGPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package util - -import ( - "net/url" - "strings" - - "golang.org/x/net/idna" -) - -// Punify converts the given domain to lowercase -// then to punycode (for international domain names). -// -// Returns the resulting domain or an error if the -// punycode conversion fails. -func Punify(domain string) (string, error) { - domain = strings.ToLower(domain) - return idna.ToASCII(domain) -} - -// DePunify converts the given punycode string -// to its original unicode representation (lowercased). -// Noop if the domain is (already) not puny. -// -// Returns an error if conversion fails. -func DePunify(domain string) (string, error) { - out, err := idna.ToUnicode(domain) - return strings.ToLower(out), err -} - -// URIMatches returns true if the expected URI matches -// any of the given URIs, taking account of punycode. -func URIMatches(expect *url.URL, uris ...*url.URL) (bool, error) { - // Normalize expect to punycode. - expectStr, err := PunifyURIToStr(expect) - if err != nil { - return false, err - } - - for _, uri := range uris { - uriStr, err := PunifyURIToStr(uri) - if err != nil { - return false, err - } - - if uriStr == expectStr { - // Looks good. - return true, nil - } - } - - // Didn't match. - return false, nil -} - -// PunifyURI returns a copy of the given URI -// with the 'host' part converted to punycode. -func PunifyURI(in *url.URL) (*url.URL, error) { - punyHost, err := Punify(in.Host) - if err != nil { - return nil, err - } - out := new(url.URL) - *out = *in - out.Host = punyHost - return out, nil -} - -// PunifyURIToStr returns given URI serialized -// with the 'host' part converted to punycode. -func PunifyURIToStr(in *url.URL) (string, error) { - punyHost, err := Punify(in.Host) - if err != nil { - return "", err - } - oldHost := in.Host - in.Host = punyHost - str := in.String() - in.Host = oldHost - return str, nil -} -- cgit v1.2.3