diff --git a/integrations/git_smart_http_test.go b/integrations/git_smart_http_test.go
new file mode 100644
index 0000000000..9a4e3689c1
--- /dev/null
+++ b/integrations/git_smart_http_test.go
@@ -0,0 +1,69 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integrations
+
+import (
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGitSmartHTTP(t *testing.T) {
+	onGiteaRun(t, testGitSmartHTTP)
+}
+
+func testGitSmartHTTP(t *testing.T, u *url.URL) {
+	var kases = []struct {
+		p    string
+		code int
+	}{
+		{
+			p:    "user2/repo1/info/refs",
+			code: 200,
+		},
+		{
+			p:    "user2/repo1/HEAD",
+			code: 200,
+		},
+		{
+			p:    "user2/repo1/objects/info/alternates",
+			code: 404,
+		},
+		{
+			p:    "user2/repo1/objects/info/http-alternates",
+			code: 404,
+		},
+		{
+			p:    "user2/repo1/../../custom/conf/app.ini",
+			code: 404,
+		},
+		{
+			p:    "user2/repo1/objects/info/../../../../custom/conf/app.ini",
+			code: 404,
+		},
+		{
+			p:    `user2/repo1/objects/info/..\..\..\..\custom\conf\app.ini`,
+			code: 400,
+		},
+	}
+
+	for _, kase := range kases {
+		t.Run(kase.p, func(t *testing.T) {
+			p := u.String() + kase.p
+			req, err := http.NewRequest("GET", p, nil)
+			assert.NoError(t, err)
+			req.SetBasicAuth("user2", userPassword)
+			resp, err := http.DefaultClient.Do(req)
+			assert.NoError(t, err)
+			defer resp.Body.Close()
+			assert.EqualValues(t, kase.code, resp.StatusCode)
+			_, err = ioutil.ReadAll(resp.Body)
+			assert.NoError(t, err)
+		})
+	}
+}
diff --git a/routers/web/repo/http.go b/routers/web/repo/http.go
index 30d382b8ef..649d6d1eb1 100644
--- a/routers/web/repo/http.go
+++ b/routers/web/repo/http.go
@@ -366,7 +366,26 @@ func (h *serviceHandler) setHeaderCacheForever() {
 	h.w.Header().Set("Cache-Control", "public, max-age=31536000")
 }
 
+func containsParentDirectorySeparator(v string) bool {
+	if !strings.Contains(v, "..") {
+		return false
+	}
+	for _, ent := range strings.FieldsFunc(v, isSlashRune) {
+		if ent == ".." {
+			return true
+		}
+	}
+	return false
+}
+
+func isSlashRune(r rune) bool { return r == '/' || r == '\\' }
+
 func (h *serviceHandler) sendFile(contentType, file string) {
+	if containsParentDirectorySeparator(file) {
+		log.Error("request file path contains invalid path: %v", file)
+		h.w.WriteHeader(http.StatusBadRequest)
+		return
+	}
 	reqFile := path.Join(h.dir, file)
 
 	fi, err := os.Stat(reqFile)
diff --git a/routers/web/repo/http_test.go b/routers/web/repo/http_test.go
new file mode 100644
index 0000000000..58ac1c07a1
--- /dev/null
+++ b/routers/web/repo/http_test.go
@@ -0,0 +1,43 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package repo
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestContainsParentDirectorySeparator(t *testing.T) {
+	tests := []struct {
+		v string
+		b bool
+	}{
+		{
+			v: `user2/repo1/info/refs`,
+			b: false,
+		},
+		{
+			v: `user2/repo1/HEAD`,
+			b: false,
+		},
+		{
+			v: `user2/repo1/some.../strange_file...mp3`,
+			b: false,
+		},
+		{
+			v: `user2/repo1/../../custom/conf/app.ini`,
+			b: true,
+		},
+		{
+			v: `user2/repo1/objects/info/..\..\..\..\custom\conf\app.ini`,
+			b: true,
+		},
+	}
+
+	for i := range tests {
+		assert.EqualValues(t, tests[i].b, containsParentDirectorySeparator(tests[i].v))
+	}
+}