CVE-2020-36627 (published 2022-12-25T11:15Z)

A vulnerability was found in Macaron i18n. It has been declared as problematic. Affected by this vulnerability is an unknown functionality of the file i18n.go. The manipulation leads to open redirect. The attack can be launched remotely. Upgrading to version 0.5.0 is able to address this issue. The name of the patch is 329b0c4844cc16a5a253c011b55180598e707735. It is recommended to upgrade the affected component. The identifier VDB-216745 was assigned to this vulnerability.

CVSS v3 base score: 6.1, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N)

Weakness information:
cwe_id: CWE-601
cwe_name: URL Redirection to Untrusted Site ('Open Redirect')
description: A web application accepts a user-controlled input that specifies a link to an external site, and uses that link in a Redirect. This simplifies phishing attacks.
extended_description: An http parameter may contain a URL value and could cause the web application to redirect the request to the specified URL. By modifying the URL value to a malicious site, an attacker may successfully launch a phishing scam and steal user credentials. Because the server name in the modified link is identical to the original site, phishing attempts have a more trustworthy appearance. Whether this issue poses a vulnerability will be subject to the intended behavior of the application. For example, a search engine might intentionally provide redirects to arbitrary URLs.
         

Fix information:
repo_url: https://github.com/go-macaron/i18n
commit_details: repo_url: https://github.com/go-macaron/i18n
author: ᴜɴᴋɴᴡᴏɴ
author_date: 2020-02-17 00:13:52+08:00
msg: security: fix Open Redirection vulnerability
repository: repo_url: https://github.com/go-macaron/i18n
repo_name: go-macaron/i18n
description: Package i18n provides an Internationalization and Localization middleware for Macaron applications
repo_language: Go
owner: go-macaron
file_changes: filename: .gitignore
change_type: ModificationType.ADD
diff: @@ -0,0 +1 @@
+/.idea

programming_language: SQL
filename: go.mod
change_type: ModificationType.MODIFY
diff: @@ -3,8 +3,7 @@ module github.com/go-macaron/i18n
 go 1.12
 
 require (
-	github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337
-	github.com/unknwon/com v0.0.0-20190804042917-757f69c95f3e
+	github.com/stretchr/testify v1.4.0
 	github.com/unknwon/i18n v0.0.0-20190805065654-5c6446a380b6
 	golang.org/x/text v0.3.2
 	gopkg.in/macaron.v1 v1.3.4

programming_language: Batchfile
filename: go.sum
change_type: ModificationType.MODIFY
diff: @@ -1,21 +1,26 @@
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191 h1:NjHlg70DuOkcAMqgt0+XA+NHwtu66MkTVVgR4fFWbcI=
 github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191/go.mod h1:VFI2o2q9kYsC4o7VP1HrEVosiZZTd+MVT3YZx4gqvJw=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
-github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
 github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4=
 github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
-github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY=
 github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w=
 github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
 github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
 github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8=
 github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/unknwon/com v0.0.0-20190804042917-757f69c95f3e h1:GSGeB9EAKY2spCABz6xOX5DbxZEXolK+nBSvmsQwRjM=
 github.com/unknwon/com v0.0.0-20190804042917-757f69c95f3e/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM=
 github.com/unknwon/i18n v0.0.0-20190805065654-5c6446a380b6 h1:sRrkJEHtNoaSvyXMbRgofEOX4/3gMiraevQKJdIBhYE=
@@ -37,7 +42,11 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190802220118-1d1727260058/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/ini.v1 v1.46.0 h1:VeDZbLYGaupuvIrsYCEOe/L/2Pcs5n7hdO1ZTjporag=
 gopkg.in/ini.v1 v1.46.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/macaron.v1 v1.3.4 h1:HvIscOwxhFhx3swWM/979wh2QMYyuXrNmrF9l+j3HZs=
 gopkg.in/macaron.v1 v1.3.4/go.mod h1:/RoHTdC8ALpyJ3+QR36mKjwnT1F1dyYtsGM9Ate6ZFI=
+gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

programming_language: Go
filename: i18n.go
change_type: ModificationType.MODIFY
diff: @@ -17,19 +17,23 @@ package i18n
 
 import (
 	"fmt"
+	"os"
 	"path"
 	"strings"
 
-	"github.com/unknwon/com"
 	"github.com/unknwon/i18n"
 	"golang.org/x/text/language"
 	"gopkg.in/macaron.v1"
 )
 
-const _VERSION = "0.4.1"
-
-func Version() string {
-	return _VERSION
+// isFile returns true if given path is a file,
+// or returns false when it's a directory or does not exist.
+func isFile(filePath string) bool {
+	f, e := os.Stat(filePath)
+	if e != nil {
+		return false
+	}
+	return !f.IsDir()
 }
 
 // initLocales initializes language type list and Accept-Language header matcher.
@@ -41,7 +45,7 @@ func initLocales(opt Options) language.Matcher {
 		// Append custom locale file.
 		custom := []interface{}{}
 		customPath := path.Join(opt.CustomDirectory, fname)
-		if com.IsFile(customPath) {
+		if isFile(customPath) {
 			custom = append(custom, customPath)
 		}
 
@@ -221,7 +225,7 @@ func I18n(options ...Options) macaron.Handler {
 		ctx.Data["RestLangs"] = restLangs
 
 		if opt.Redirect && isNeedRedir {
-			ctx.Redirect(opt.SubURL + ctx.Req.RequestURI[:strings.Index(ctx.Req.RequestURI, "?")])
+			ctx.Redirect(opt.SubURL + path.Clean(ctx.Req.RequestURI[:strings.Index(ctx.Req.RequestURI, "?")]))
 		}
 	}
 }

programming_language: Go
filename: i18n_test.go
change_type: ModificationType.MODIFY
diff: @@ -15,118 +15,144 @@
 package i18n
 
 import (
+	"errors"
 	"net/http"
 	"net/http/httptest"
 	"testing"
 
-	. "github.com/smartystreets/goconvey/convey"
+	"github.com/stretchr/testify/assert"
 	"gopkg.in/macaron.v1"
 )
 
-func Test_Version(t *testing.T) {
-	Convey("Check package version", t, func() {
-		So(Version(), ShouldEqual, _VERSION)
-	})
-}
-
-func Test_I18n(t *testing.T) {
-	Convey("Use i18n middleware", t, func() {
-		Convey("No langauge", func() {
-			defer func() {
-				So(recover(), ShouldNotBeNil)
-			}()
+func TestI18n(t *testing.T) {
+	t.Run("no language", func(t *testing.T) {
+		defer func() {
+			assert.Equal(t, "no language is specified", recover())
+		}()
 
-			m := macaron.New()
-			m.Use(I18n(Options{}))
-		})
+		m := macaron.New()
+		m.Use(I18n(Options{}))
+	})
 
-		Convey("Languages and names not match", func() {
-			defer func() {
-				So(recover(), ShouldNotBeNil)
-			}()
+	t.Run("languages and names not match", func(t *testing.T) {
+		defer func() {
+			assert.Equal(t, "length of langs is not same as length of names", recover())
+		}()
 
-			m := macaron.New()
-			m.Use(I18n(Options{
-				Langs: []string{"en-US"},
-			}))
-		})
+		m := macaron.New()
+		m.Use(I18n(Options{
+			Langs: []string{"en-US"},
+		}))
+	})
 
-		Convey("Invalid directory", func() {
-			defer func() {
-				So(recover(), ShouldNotBeNil)
-			}()
-
-			m := macaron.New()
-			m.Use(I18n(Options{
-				Directory: "404",
-				Langs:     []string{"en-US"},
-				Names:     []string{"English"},
-			}))
-		})
+	t.Run("invalid directory", func(t *testing.T) {
+		defer func() {
+			assert.Equal(t, errors.New("fail to set message file(en-US): open 404/locale_en-US.ini: no such file or directory"), recover())
+		}()
+
+		m := macaron.New()
+		m.Use(I18n(Options{
+			Directory: "404",
+			Langs:     []string{"en-US"},
+			Names:     []string{"English"},
+		}))
+	})
 
-		Convey("With correct options", func() {
-			m := macaron.New()
-			m.Use(I18n(Options{
-				Files: map[string][]byte{"locale_en-US.ini": []byte("")},
-				Langs: []string{"en-US"},
-				Names: []string{"English"},
-			}))
-			m.Get("/", func() {})
+	t.Run("with correct options", func(t *testing.T) {
+		m := macaron.New()
+		m.Use(I18n(Options{
+			Files: map[string][]byte{"locale_en-US.ini": []byte("")},
+			Langs: []string{"en-US"},
+			Names: []string{"English"},
+		}))
+		m.Get("/", func() {})
+
+		resp := httptest.NewRecorder()
+		req, err := http.NewRequest("GET", "/", nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+		m.ServeHTTP(resp, req)
+	})
 
-			resp := httptest.NewRecorder()
-			req, err := http.NewRequest("GET", "/", nil)
-			So(err, ShouldBeNil)
-			m.ServeHTTP(resp, req)
+	t.Run("set by Accept-Language", func(t *testing.T) {
+		m := macaron.New()
+		m.Use(I18n(Options{
+			Langs: []string{"en-US", "zh-CN", "it-IT"},
+			Names: []string{"English", "简体中文", "Italiano"},
+		}))
+		m.Get("/", func(l Locale) {
+			assert.Equal(t, "it-IT", l.Language())
 		})
 
-		Convey("Set by redirect of URL parameter", func() {
-			m := macaron.New()
-			m.Use(I18n(Options{
-				Langs:    []string{"en-US"},
-				Names:    []string{"English"},
-				Redirect: true,
-			}))
-			m.Get("/", func() {})
+		resp := httptest.NewRecorder()
+		req, err := http.NewRequest("GET", "/", nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+		req.Header.Set("Accept-Language", "it")
+		m.ServeHTTP(resp, req)
+	})
 
-			resp := httptest.NewRecorder()
-			req, err := http.NewRequest("GET", "/?lang=en-US", nil)
-			So(err, ShouldBeNil)
-			req.RequestURI = "/?lang=en-US"
-			m.ServeHTTP(resp, req)
+	t.Run("set to default language", func(t *testing.T) {
+		m := macaron.New()
+		m.Use(I18n(Options{
+			Langs: []string{"en-US", "zh-CN", "it-IT"},
+			Names: []string{"English", "简体中文", "Italiano"},
+		}))
+		m.Get("/", func(l Locale) {
+			assert.Equal(t, "en-US", l.Language())
 		})
 
-		Convey("Set by Accept-Language", func() {
-			m := macaron.New()
-			m.Use(I18n(Options{
-				Langs: []string{"en-US", "zh-CN", "it-IT"},
-				Names: []string{"English", "简体中文", "Italiano"},
-			}))
-			m.Get("/", func(l Locale) {
-				So(l.Language(), ShouldEqual, "it-IT")
-			})
+		resp := httptest.NewRecorder()
+		req, err := http.NewRequest("GET", "/", nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+		req.Header.Set("Accept-Language", "ru")
+		m.ServeHTTP(resp, req)
+	})
+}
 
+func TestRedirect(t *testing.T) {
+	m := macaron.New()
+	m.Use(I18n(Options{
+		Langs:    []string{"en-US"},
+		Names:    []string{"English"},
+		Redirect: true,
+	}))
+	m.Get("/", func() {})
+
+	tests := []struct {
+		url    string
+		expURL string
+	}{
+		{
+			url:    "/?lang=en-US",
+			expURL: "/",
+		}, {
+			url:    "//example.com?lang=en-US",
+			expURL: "/example.com",
+		}, {
+			url:    "/abc/../../../example.com?lang=en-US",
+			expURL: "/example.com",
+		}, {
+			url:    "/../abc/../example.com?lang=en-US",
+			expURL: "/example.com",
+		},
+	}
+	for _, test := range tests {
+		t.Run("", func(t *testing.T) {
 			resp := httptest.NewRecorder()
-			req, err := http.NewRequest("GET", "/", nil)
-			So(err, ShouldBeNil)
-			req.Header.Set("Accept-Language", "it")
+			req, err := http.NewRequest("GET", test.url, nil)
+			if err != nil {
+				t.Fatal(err)
+			}
+			req.RequestURI = test.url
 			m.ServeHTTP(resp, req)
-		})
-
-		Convey("Set to default language", func() {
-			m := macaron.New()
-			m.Use(I18n(Options{
-				Langs: []string{"en-US", "zh-CN", "it-IT"},
-				Names: []string{"English", "简体中文", "Italiano"},
-			}))
-			m.Get("/", func(l Locale) {
-				So(l.Language(), ShouldEqual, "en-US")
-			})
 
-			resp := httptest.NewRecorder()
-			req, err := http.NewRequest("GET", "/", nil)
-			So(err, ShouldBeNil)
-			req.Header.Set("Accept-Language", "ru")
-			m.ServeHTTP(resp, req)
+			assert.Equal(t, http.StatusFound, resp.Code)
+			assert.Equal(t, "<a href=\""+test.expURL+"\">Found</a>.\n\n", resp.Body.String())
 		})
-	})
+	}
 }

programming_language: Go