time: bound file reads and validate LoadLocation argument

Fixes #18985

Change-Id: I956117f47d1d2b453b4786c7b78c1c944defeca0
Reviewed-on: https://go-review.googlesource.com/36551
Reviewed-by: Ian Lance Taylor <iant@golang.org>
diff --git a/src/time/export_test.go b/src/time/export_test.go
index 26584b5..4c08ab1 100644
--- a/src/time/export_test.go
+++ b/src/time/export_test.go
@@ -32,4 +32,6 @@
 	ParseTimeZone          = parseTimeZone
 	SetMono                = (*Time).setMono
 	GetMono                = (*Time).mono
+	ErrLocation            = errLocation
+	ReadFile               = readFile
 )
diff --git a/src/time/sys_plan9.go b/src/time/sys_plan9.go
index 11365a7..9086a6e 100644
--- a/src/time/sys_plan9.go
+++ b/src/time/sys_plan9.go
@@ -19,6 +19,7 @@
 // readFile reads and returns the content of the named file.
 // It is a trivial implementation of ioutil.ReadFile, reimplemented
 // here to avoid depending on io/ioutil or os.
+// It returns an error if name exceeds maxFileSize bytes.
 func readFile(name string) ([]byte, error) {
 	f, err := syscall.Open(name, syscall.O_RDONLY)
 	if err != nil {
@@ -38,6 +39,9 @@
 		if n == 0 || err != nil {
 			break
 		}
+		if len(ret) > maxFileSize {
+			return nil, fileSizeError(name)
+		}
 	}
 	return ret, err
 }
diff --git a/src/time/sys_unix.go b/src/time/sys_unix.go
index 91d54c9..d4db8f9 100644
--- a/src/time/sys_unix.go
+++ b/src/time/sys_unix.go
@@ -19,6 +19,7 @@
 // readFile reads and returns the content of the named file.
 // It is a trivial implementation of ioutil.ReadFile, reimplemented
 // here to avoid depending on io/ioutil or os.
+// It returns an error if name exceeds maxFileSize bytes.
 func readFile(name string) ([]byte, error) {
 	f, err := syscall.Open(name, syscall.O_RDONLY, 0)
 	if err != nil {
@@ -38,6 +39,9 @@
 		if n == 0 || err != nil {
 			break
 		}
+		if len(ret) > maxFileSize {
+			return nil, fileSizeError(name)
+		}
 	}
 	return ret, err
 }
diff --git a/src/time/sys_windows.go b/src/time/sys_windows.go
index a4a068f..9e38165 100644
--- a/src/time/sys_windows.go
+++ b/src/time/sys_windows.go
@@ -16,6 +16,7 @@
 // readFile reads and returns the content of the named file.
 // It is a trivial implementation of ioutil.ReadFile, reimplemented
 // here to avoid depending on io/ioutil or os.
+// It returns an error if name exceeds maxFileSize bytes.
 func readFile(name string) ([]byte, error) {
 	f, err := syscall.Open(name, syscall.O_RDONLY, 0)
 	if err != nil {
@@ -35,6 +36,9 @@
 		if n == 0 || err != nil {
 			break
 		}
+		if len(ret) > maxFileSize {
+			return nil, fileSizeError(name)
+		}
 	}
 	return ret, err
 }
diff --git a/src/time/time_test.go b/src/time/time_test.go
index 2922560..90e2abf 100644
--- a/src/time/time_test.go
+++ b/src/time/time_test.go
@@ -11,7 +11,9 @@
 	"fmt"
 	"math/big"
 	"math/rand"
+	"os"
 	"runtime"
+	"strings"
 	"testing"
 	"testing/quick"
 	. "time"
@@ -1254,3 +1256,14 @@
 		t.Errorf("zero month = %q; want %q", got, want)
 	}
 }
+
+func TestReadFileLimit(t *testing.T) {
+	const zero = "/dev/zero"
+	if _, err := os.Stat(zero); err != nil {
+		t.Skip("skipping test without a /dev/zero")
+	}
+	_, err := ReadFile(zero)
+	if err == nil || !strings.Contains(err.Error(), "is too large") {
+		t.Errorf("readFile(%q) error = %v; want error containing 'is too large'", zero, err)
+	}
+}
diff --git a/src/time/zoneinfo.go b/src/time/zoneinfo.go
index 7cde142..dfe857f 100644
--- a/src/time/zoneinfo.go
+++ b/src/time/zoneinfo.go
@@ -5,6 +5,7 @@
 package time
 
 import (
+	"errors"
 	"sync"
 	"syscall"
 )
@@ -256,6 +257,8 @@
 // NOTE(rsc): Eventually we will need to accept the POSIX TZ environment
 // syntax too, but I don't feel like implementing it today.
 
+var errLocation = errors.New("time: invalid location name")
+
 var zoneinfo *string
 var zoneinfoOnce sync.Once
 
@@ -280,6 +283,11 @@
 	if name == "Local" {
 		return Local, nil
 	}
+	if containsDotDot(name) || name[0] == '/' || name[0] == '\\' {
+		// No valid IANA Time Zone name contains a single dot,
+		// much less dot dot. Likewise, none begin with a slash.
+		return nil, errLocation
+	}
 	zoneinfoOnce.Do(func() {
 		env, _ := syscall.Getenv("ZONEINFO")
 		zoneinfo = &env
@@ -292,3 +300,16 @@
 	}
 	return loadLocation(name)
 }
+
+// containsDotDot reports whether s contains "..".
+func containsDotDot(s string) bool {
+	if len(s) < 2 {
+		return false
+	}
+	for i := 0; i < len(s)-1; i++ {
+		if s[i] == '.' && s[i+1] == '.' {
+			return true
+		}
+	}
+	return false
+}
diff --git a/src/time/zoneinfo_read.go b/src/time/zoneinfo_read.go
index 1b3356e..b0cd9da 100644
--- a/src/time/zoneinfo_read.go
+++ b/src/time/zoneinfo_read.go
@@ -11,6 +11,17 @@
 
 import "errors"
 
+// maxFileSize is the max permitted size of files read by readFile.
+// As reference, the zoneinfo.zip distributed by Go is ~350 KB,
+// so 10MB is overkill.
+const maxFileSize = 10 << 20
+
+type fileSizeError string
+
+func (f fileSizeError) Error() string {
+	return "time: file " + string(f) + " is too large"
+}
+
 // Copies of io.Seek* constants to avoid importing "io":
 const (
 	seekStart   = 0
diff --git a/src/time/zoneinfo_test.go b/src/time/zoneinfo_test.go
index e388e99..b25733c 100644
--- a/src/time/zoneinfo_test.go
+++ b/src/time/zoneinfo_test.go
@@ -20,8 +20,8 @@
 func TestEnvVarUsage(t *testing.T) {
 	time.ResetZoneinfoForTesting()
 
-	testZoneinfo := "foo.zip"
-	env := "ZONEINFO"
+	const testZoneinfo = "foo.zip"
+	const env = "ZONEINFO"
 
 	defer os.Setenv(env, os.Getenv(env))
 	os.Setenv(env, testZoneinfo)
@@ -35,6 +35,26 @@
 	}
 }
 
+func TestLoadLocationValidatesNames(t *testing.T) {
+	time.ResetZoneinfoForTesting()
+	const env = "ZONEINFO"
+	defer os.Setenv(env, os.Getenv(env))
+	os.Setenv(env, "")
+
+	bad := []string{
+		"/usr/foo/Foo",
+		"\\UNC\foo",
+		"..",
+		"a..",
+	}
+	for _, v := range bad {
+		_, err := time.LoadLocation(v)
+		if err != time.ErrLocation {
+			t.Errorf("LoadLocation(%q) error = %v; want ErrLocation", v, err)
+		}
+	}
+}
+
 func TestVersion3(t *testing.T) {
 	time.ForceZipFileForTesting(true)
 	defer time.ForceZipFileForTesting(false)