mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-23 22:27:38 -05:00
tls: Prevent directory traversal via On-Demand TLS (fixes #2092)
This commit is contained in:
parent
858e96f21c
commit
73b61af58d
5 changed files with 113 additions and 15 deletions
|
@ -257,6 +257,13 @@ func (c *ACMEClient) Obtain(name string) error {
|
||||||
return errors.New(errMsg)
|
return errors.New(errMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// double-check that we actually got a certificate; check a couple fields
|
||||||
|
// TODO: This is a temporary workaround for what I think is a bug in the acmev2 package (March 2018)
|
||||||
|
// but it might not hurt to keep this extra check in place
|
||||||
|
if certificate.Domain == "" || certificate.Certificate == nil {
|
||||||
|
return errors.New("returned certificate was empty; probably an unchecked error obtaining it")
|
||||||
|
}
|
||||||
|
|
||||||
// Success - immediately save the certificate resource
|
// Success - immediately save the certificate resource
|
||||||
err = saveCertResource(c.storage, certificate)
|
err = saveCertResource(c.storage, certificate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -311,9 +318,16 @@ func (c *ACMEClient) Renew(name string) error {
|
||||||
acmeMu.Unlock()
|
acmeMu.Unlock()
|
||||||
namesObtaining.Remove([]string{name})
|
namesObtaining.Remove([]string{name})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
// double-check that we actually got a certificate; check a couple fields
|
||||||
|
// TODO: This is a temporary workaround for what I think is a bug in the acmev2 package (March 2018)
|
||||||
|
// but it might not hurt to keep this extra check in place
|
||||||
|
if newCertMeta.Domain == "" || newCertMeta.Certificate == nil {
|
||||||
|
err = errors.New("returned certificate was empty; probably an unchecked error renewing it")
|
||||||
|
} else {
|
||||||
success = true
|
success = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// wait a little bit and try again
|
// wait a little bit and try again
|
||||||
wait := 10 * time.Second
|
wait := 10 * time.Second
|
||||||
|
|
|
@ -58,30 +58,25 @@ func (s *FileStorage) sites() string {
|
||||||
|
|
||||||
// site returns the path to the folder containing assets for domain.
|
// site returns the path to the folder containing assets for domain.
|
||||||
func (s *FileStorage) site(domain string) string {
|
func (s *FileStorage) site(domain string) string {
|
||||||
// Windows doesn't allow * in filenames, sigh...
|
domain = fileSafe(domain)
|
||||||
domain = strings.Replace(domain, "*", "wildcard_", -1)
|
|
||||||
domain = strings.ToLower(domain)
|
|
||||||
return filepath.Join(s.sites(), domain)
|
return filepath.Join(s.sites(), domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
// siteCertFile returns the path to the certificate file for domain.
|
// siteCertFile returns the path to the certificate file for domain.
|
||||||
func (s *FileStorage) siteCertFile(domain string) string {
|
func (s *FileStorage) siteCertFile(domain string) string {
|
||||||
domain = strings.Replace(domain, "*", "wildcard_", -1)
|
domain = fileSafe(domain)
|
||||||
domain = strings.ToLower(domain)
|
|
||||||
return filepath.Join(s.site(domain), domain+".crt")
|
return filepath.Join(s.site(domain), domain+".crt")
|
||||||
}
|
}
|
||||||
|
|
||||||
// siteKeyFile returns the path to domain's private key file.
|
// siteKeyFile returns the path to domain's private key file.
|
||||||
func (s *FileStorage) siteKeyFile(domain string) string {
|
func (s *FileStorage) siteKeyFile(domain string) string {
|
||||||
domain = strings.Replace(domain, "*", "wildcard_", -1)
|
domain = fileSafe(domain)
|
||||||
domain = strings.ToLower(domain)
|
|
||||||
return filepath.Join(s.site(domain), domain+".key")
|
return filepath.Join(s.site(domain), domain+".key")
|
||||||
}
|
}
|
||||||
|
|
||||||
// siteMetaFile returns the path to the domain's asset metadata file.
|
// siteMetaFile returns the path to the domain's asset metadata file.
|
||||||
func (s *FileStorage) siteMetaFile(domain string) string {
|
func (s *FileStorage) siteMetaFile(domain string) string {
|
||||||
domain = strings.Replace(domain, "*", "wildcard_", -1)
|
domain = fileSafe(domain)
|
||||||
domain = strings.ToLower(domain)
|
|
||||||
return filepath.Join(s.site(domain), domain+".json")
|
return filepath.Join(s.site(domain), domain+".json")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,7 +90,7 @@ func (s *FileStorage) user(email string) string {
|
||||||
if email == "" {
|
if email == "" {
|
||||||
email = emptyEmail
|
email = emptyEmail
|
||||||
}
|
}
|
||||||
email = strings.ToLower(email)
|
email = fileSafe(email)
|
||||||
return filepath.Join(s.users(), email)
|
return filepath.Join(s.users(), email)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,6 +117,7 @@ func (s *FileStorage) userRegFile(email string) string {
|
||||||
if fileName == "" {
|
if fileName == "" {
|
||||||
fileName = "registration"
|
fileName = "registration"
|
||||||
}
|
}
|
||||||
|
fileName = fileSafe(fileName)
|
||||||
return filepath.Join(s.user(email), fileName+".json")
|
return filepath.Join(s.user(email), fileName+".json")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,6 +132,7 @@ func (s *FileStorage) userKeyFile(email string) string {
|
||||||
if fileName == "" {
|
if fileName == "" {
|
||||||
fileName = "private"
|
fileName = "private"
|
||||||
}
|
}
|
||||||
|
fileName = fileSafe(fileName)
|
||||||
return filepath.Join(s.user(email), fileName+".key")
|
return filepath.Join(s.user(email), fileName+".key")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,3 +276,29 @@ func (s *FileStorage) MostRecentUserEmail() string {
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fileSafe standardizes and sanitizes str for use in a file path.
|
||||||
|
func fileSafe(str string) string {
|
||||||
|
str = strings.ToLower(str)
|
||||||
|
str = strings.TrimSpace(str)
|
||||||
|
repl := strings.NewReplacer("..", "",
|
||||||
|
"/", "",
|
||||||
|
"\\", "",
|
||||||
|
// TODO: Consider also replacing "@" with "_at_" (but migrate existing accounts...)
|
||||||
|
"+", "_plus_",
|
||||||
|
"%", "",
|
||||||
|
"$", "",
|
||||||
|
"`", "",
|
||||||
|
"~", "",
|
||||||
|
":", "",
|
||||||
|
";", "",
|
||||||
|
"=", "",
|
||||||
|
"!", "",
|
||||||
|
"#", "",
|
||||||
|
"&", "",
|
||||||
|
"|", "",
|
||||||
|
"\"", "",
|
||||||
|
"'", "",
|
||||||
|
"*", "wildcard_")
|
||||||
|
return repl.Replace(str)
|
||||||
|
}
|
||||||
|
|
|
@ -14,7 +14,54 @@
|
||||||
|
|
||||||
package caddytls
|
package caddytls
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
// *********************************** NOTE ********************************
|
// *********************************** NOTE ********************************
|
||||||
// Due to circular package dependencies with the storagetest sub package and
|
// Due to circular package dependencies with the storagetest sub package and
|
||||||
// the fact that we want to use that harness to test file storage, the tests
|
// the fact that we want to use that harness to test file storage, most of
|
||||||
// for file storage are done in the storagetest package.
|
// the tests for file storage are done in the storagetest package.
|
||||||
|
|
||||||
|
func TestPathBuilders(t *testing.T) {
|
||||||
|
fs := FileStorage{Path: "/test"}
|
||||||
|
|
||||||
|
for i, testcase := range []struct {
|
||||||
|
in, folder, certFile, keyFile, metaFile string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
in: "example.com",
|
||||||
|
folder: "/test/sites/example.com",
|
||||||
|
certFile: "/test/sites/example.com/example.com.crt",
|
||||||
|
keyFile: "/test/sites/example.com/example.com.key",
|
||||||
|
metaFile: "/test/sites/example.com/example.com.json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: "*.example.com",
|
||||||
|
folder: "/test/sites/wildcard_.example.com",
|
||||||
|
certFile: "/test/sites/wildcard_.example.com/wildcard_.example.com.crt",
|
||||||
|
keyFile: "/test/sites/wildcard_.example.com/wildcard_.example.com.key",
|
||||||
|
metaFile: "/test/sites/wildcard_.example.com/wildcard_.example.com.json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// prevent directory traversal! very important, esp. with on-demand TLS
|
||||||
|
// see issue #2092
|
||||||
|
in: "a/../../../foo",
|
||||||
|
folder: "/test/sites/afoo",
|
||||||
|
certFile: "/test/sites/afoo/afoo.crt",
|
||||||
|
keyFile: "/test/sites/afoo/afoo.key",
|
||||||
|
metaFile: "/test/sites/afoo/afoo.json",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
if actual := fs.site(testcase.in); actual != testcase.folder {
|
||||||
|
t.Errorf("Test %d: site folder: Expected '%s' but got '%s'", i, testcase.folder, actual)
|
||||||
|
}
|
||||||
|
if actual := fs.siteCertFile(testcase.in); actual != testcase.certFile {
|
||||||
|
t.Errorf("Test %d: site cert file: Expected '%s' but got '%s'", i, testcase.certFile, actual)
|
||||||
|
}
|
||||||
|
if actual := fs.siteKeyFile(testcase.in); actual != testcase.keyFile {
|
||||||
|
t.Errorf("Test %d: site key file: Expected '%s' but got '%s'", i, testcase.keyFile, actual)
|
||||||
|
}
|
||||||
|
if actual := fs.siteMetaFile(testcase.in); actual != testcase.metaFile {
|
||||||
|
t.Errorf("Test %d: site meta file: Expected '%s' but got '%s'", i, testcase.metaFile, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -91,7 +91,20 @@ func (s *fileStorageLock) Unlock(name string) error {
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("FileStorage: no lock to release for %s", name)
|
return fmt.Errorf("FileStorage: no lock to release for %s", name)
|
||||||
}
|
}
|
||||||
|
// remove lock file
|
||||||
os.Remove(fw.filename)
|
os.Remove(fw.filename)
|
||||||
|
|
||||||
|
// if parent folder is now empty, remove it too to keep it tidy
|
||||||
|
lockParentFolder := s.storage.site(name)
|
||||||
|
dir, err := os.Open(lockParentFolder)
|
||||||
|
if err == nil {
|
||||||
|
items, _ := dir.Readdirnames(3) // OK to ignore error here
|
||||||
|
if len(items) == 0 {
|
||||||
|
os.Remove(lockParentFolder)
|
||||||
|
}
|
||||||
|
dir.Close()
|
||||||
|
}
|
||||||
|
|
||||||
fw.wg.Done()
|
fw.wg.Done()
|
||||||
delete(fileStorageNameLocks, s.caURL+name)
|
delete(fileStorageNameLocks, s.caURL+name)
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -58,7 +58,8 @@ type Locker interface {
|
||||||
// successfully obtained the lock (no Waiter value was returned)
|
// successfully obtained the lock (no Waiter value was returned)
|
||||||
// should call this method, and it should be called only after
|
// should call this method, and it should be called only after
|
||||||
// the obtain/renew and store are finished, even if there was
|
// the obtain/renew and store are finished, even if there was
|
||||||
// an error (or a timeout).
|
// an error (or a timeout). Unlock should also clean up any
|
||||||
|
// unused resources allocated during TryLock.
|
||||||
Unlock(name string) error
|
Unlock(name string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue