Skip to content

Commit 9fdb79a

Browse files
add support for escaping colon and caret in paths
1 parent bde73fb commit 9fdb79a

File tree

7 files changed

+195
-36
lines changed

7 files changed

+195
-36
lines changed

main.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func connect(auth bool) *vault.Vault {
8989
return v
9090
}
9191

92-
//Exits program with error if no Vault targeted
92+
// Exits program with error if no Vault targeted
9393
func getVaultURL() string {
9494
ret := os.Getenv("VAULT_ADDR")
9595
if ret == "" {
@@ -2061,7 +2061,7 @@ paths/keys.
20612061
if !opt.List.Quick {
20622062
for i := range paths {
20632063
if !strings.HasSuffix(paths[i], "/") {
2064-
fullpath := path + "/" + paths[i]
2064+
fullpath := path + "/" + vault.EscapePathSegment(paths[i])
20652065
mountVersion, err := v.MountVersion(fullpath)
20662066
if err != nil {
20672067
return err
@@ -4463,7 +4463,7 @@ func recursively(cmd string, args ...string) bool {
44634463
return y == "y" || y == "yes"
44644464
}
44654465

4466-
//For versions of safe 0.10+
4466+
// For versions of safe 0.10+
44674467
// Older versions just use a map[string]map[string]string
44684468
type exportFormat struct {
44694469
ExportVersion uint `json:"export_version"`

tests

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ root_token=
1212
unseal_key=
1313

1414
declare -a versions=(
15-
"1.3.10"
16-
"1.4.7"
17-
"1.5.7"
18-
"1.6.3"
15+
"1.9.10"
16+
"1.10.10"
17+
"1.11.7"
18+
"1.12.3"
1919
)
2020

2121
case $OSTYPE in

vault/proxy.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ func getEnvironmentVariable(variables ...string) string {
141141
return ""
142142
}
143143

144-
//SOCKS5SSHConfig contains configuration variables for setting up a SOCKS5
145-
//proxy to be tunneled through an SSH connection.
144+
// SOCKS5SSHConfig contains configuration variables for setting up a SOCKS5
145+
// proxy to be tunneled through an SSH connection.
146146
type SOCKS5SSHConfig struct {
147147
Host string
148148
User string
@@ -151,7 +151,7 @@ type SOCKS5SSHConfig struct {
151151
SkipHostKeyValidation bool
152152
}
153153

154-
//StartSSHTunnel makes an SSH connection according to the given config. It
154+
// StartSSHTunnel makes an SSH connection according to the given config. It
155155
// returns an SSH client if it was successful and an error otherwise.
156156
func StartSSHTunnel(conf SOCKS5SSHConfig) (*ssh.Client, error) {
157157
hostKeyCallback := ssh.InsecureIgnoreHostKey()
@@ -186,9 +186,9 @@ func StartSSHTunnel(conf SOCKS5SSHConfig) (*ssh.Client, error) {
186186
return ssh.Dial("tcp", conf.Host, sshConfig)
187187
}
188188

189-
//StartSOCKS5SSH makes an SSH connection according to the given config, starts
190-
//a local SOCKS5 server on a random port, and then returns the proxy
191-
//address if the connection was successful and an error if it was unsuccessful.
189+
// StartSOCKS5SSH makes an SSH connection according to the given config, starts
190+
// a local SOCKS5 server on a random port, and then returns the proxy
191+
// address if the connection was successful and an error if it was unsuccessful.
192192
func StartSOCKS5Server(dialFn func(string, string) (net.Conn, error)) (string, error) {
193193
socks5Server, err := socks5.New(&socks5.Config{
194194
Dial: noopDialContext(dialFn),
@@ -302,7 +302,7 @@ func writeKnownHosts(knownHostsFile, hostname string, key ssh.PublicKey) error {
302302

303303
fileInfo, err := f.Stat()
304304
if err != nil {
305-
return fmt.Errorf("Could no retrieve info for file `%s'")
305+
return fmt.Errorf("Could not retrieve info for file `%s': %s", knownHostsFile, err)
306306
}
307307

308308
if fileInfo.Size() != 0 {

vault/tree.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
"github.com/starkandwayne/goutils/tree"
1313
)
1414

15-
//This is a synchronized queue that specifically works with our tree algorithm,
15+
// This is a synchronized queue that specifically works with our tree algorithm,
1616
// in which the workers that pull work off the queue are also responsible for
1717
// populating the queue. This is because of the recursive nature of the tree
1818
// population. All workers are released when all workers are simultaneously
@@ -142,7 +142,7 @@ func (v *Vault) ConstructSecrets(path string, opts TreeOpts) (s Secrets, err err
142142
return s, nil
143143
}
144144

145-
//This does not keep the list in a sorted order. Sort afterward
145+
// This does not keep the list in a sorted order. Sort afterward
146146
func (s *Secrets) purgeWhereLatestVersionDeleted() {
147147
for i := 0; i < len(*s); i++ {
148148
if len((*s)[i].Versions) == 0 || (*s)[i].Versions[len((*s)[i].Versions)-1].State != SecretStateAlive {
@@ -368,7 +368,7 @@ func (v *Vault) constructTree(path string, opts TreeOpts) (*secretTree, error) {
368368
return ret, err
369369
}
370370

371-
//Only use this for the base for the initial node of the tree. You can infer
371+
// Only use this for the base for the initial node of the tree. You can infer
372372
// type much faster than this if you know the operation that retrieved it in the
373373
// first place.
374374
func (t *secretTree) populateNodeType(v *Vault) error {
@@ -459,7 +459,12 @@ func (s Secrets) Paths() []string {
459459
for i := range s {
460460
if len(s[i].Versions) > 0 {
461461
for _, key := range s[i].Versions[len(s[i].Versions)-1].Data.Keys() {
462-
ret = append(ret, fmt.Sprintf("%s:%s", s[i].Path, key))
462+
ret = append(ret,
463+
fmt.Sprintf("%s:%s",
464+
EscapePathSegment(s[i].Path),
465+
EscapePathSegment(key),
466+
),
467+
)
463468
}
464469
} else {
465470
ret = append(ret, s[i].Path)

vault/utils.go

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,59 @@ import (
88
"strings"
99
)
1010

11+
var keyColonRegexp = regexp.MustCompile(`[^\\](:)`)
12+
var versionCaretRegexp = regexp.MustCompile(`[^\\](\^)`)
13+
1114
// ParsePath splits the given path string into its respective secret path
12-
// and contained key parts
15+
// and contained key parts
1316
func ParsePath(path string) (secret, key string, version uint64) {
1417
secret = path
15-
if idx := strings.LastIndex(path, "^"); idx >= 0 {
16-
versionString := path[idx+1:]
17-
var err error
18+
var err error
19+
20+
matches := versionCaretRegexp.FindAllStringSubmatchIndex(path, -1)
21+
if len(matches) > 0 { //if there exists a version caret
22+
caretIdx := matches[len(matches)-1]
23+
caretStart, caretEnd := caretIdx[len(caretIdx)-2], caretIdx[len(caretIdx)-1]
24+
versionString := path[caretEnd:]
1825
version, err = strconv.ParseUint(versionString, 10, 64)
1926
if err == nil {
20-
path = path[:idx]
27+
path = path[:caretStart]
2128
secret = path
2229
}
2330
}
2431

25-
if idx := strings.LastIndex(path, ":"); idx >= 0 {
26-
secret = path[:idx]
27-
key = path[idx+1:]
32+
matches = keyColonRegexp.FindAllStringSubmatchIndex(path, -1)
33+
if len(matches) > 0 { //if there exists a path colon
34+
colonIdx := matches[len(matches)-1]
35+
colonStart, colonEnd := colonIdx[len(colonIdx)-2], colonIdx[len(colonIdx)-1]
36+
key = path[colonEnd:]
37+
secret = path[:colonStart]
2838
}
2939

40+
//unescape escaped characters
41+
secret = strings.ReplaceAll(secret, `\:`, ":")
42+
secret = strings.ReplaceAll(secret, `\^`, "^")
43+
key = strings.ReplaceAll(key, `\:`, ":")
44+
key = strings.ReplaceAll(key, `\^`, "^")
45+
3046
secret = Canonicalize(secret)
3147
return
3248
}
3349

50+
// EscapePathSegment is the reverse of ParsePath for an output secret or key
51+
// segment; whereas that function unescapes colons and carets, this function
52+
// reescapes them so that they can be run through that function again.
53+
func EscapePathSegment(segment string) string {
54+
segment = strings.ReplaceAll(segment, ":", `\:`)
55+
segment = strings.ReplaceAll(segment, "^", `\^`)
56+
return segment
57+
}
58+
3459
// EncodePath creates a safe-friendly canonical path for the given arguments
3560
func EncodePath(path, key string, version uint64) string {
61+
path = EscapePathSegment(path)
3662
if key != "" {
63+
key = EscapePathSegment(key)
3764
path += ":" + key
3865
}
3966

vault/utils_test.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package vault_test
2+
3+
import (
4+
. "github.com/onsi/ginkgo"
5+
. "github.com/onsi/gomega"
6+
"github.com/starkandwayne/safe/vault"
7+
)
8+
9+
var _ = Describe("Utils", func() {
10+
Describe("ParsePath", func() {
11+
var inPath, inKey, inVersion string
12+
var outPath, outKey string
13+
var outVersion uint64
14+
15+
var expPath, expKey string
16+
var expVersion uint64
17+
18+
JustBeforeEach(func() {
19+
var fullInPath string = inPath
20+
if inKey != "" {
21+
fullInPath = fullInPath + ":" + inKey
22+
}
23+
if inVersion != "" {
24+
fullInPath = fullInPath + "^" + inVersion
25+
}
26+
outPath, outKey, outVersion = vault.ParsePath(fullInPath)
27+
})
28+
29+
AfterEach(func() {
30+
inPath, inKey, inVersion = "", "", ""
31+
outPath, outKey = "", ""
32+
outVersion = 0
33+
expPath, expKey = "", ""
34+
expVersion = 0
35+
})
36+
37+
assertPathValues := func() {
38+
It("should have the expected values", func() {
39+
By("having the correct path value")
40+
Expect(outPath).To(Equal(expPath))
41+
42+
By("having the correct key value")
43+
Expect(outKey).To(Equal(expKey))
44+
45+
By("having the correct version value")
46+
Expect(outVersion).To(Equal(expVersion))
47+
})
48+
}
49+
50+
type ioStruct struct{ in, out, desc string }
51+
52+
paths := []ioStruct{
53+
{"secret/foo", "secret/foo", "that is basic"},
54+
{`secret/f\:oo`, "secret/f:oo", "that has an escaped colon"},
55+
{`secret/f\^oo`, "secret/f^oo", "that has an escaped caret"},
56+
}
57+
58+
keys := []ioStruct{
59+
{"bar", "bar", "that is basic"},
60+
{`b\:ar`, "b:ar", "that has an escaped colon"},
61+
{`b\^ar`, "b^ar", "that has an escaped caret"},
62+
}
63+
64+
Context("with a path", func() {
65+
for i := range paths {
66+
path := paths[i]
67+
Context(path.desc, func() {
68+
BeforeEach(func() {
69+
inPath, expPath = path.in, path.out
70+
})
71+
72+
assertPathValues()
73+
74+
Context("with a key", func() {
75+
for j := range keys {
76+
key := keys[j]
77+
Context(key.desc, func() {
78+
BeforeEach(func() {
79+
inKey, expKey = key.in, key.out
80+
})
81+
82+
assertPathValues()
83+
84+
Context("with a version", func() {
85+
Context("that is zero", func() {
86+
BeforeEach(func() {
87+
inVersion, expVersion = "0", 0
88+
})
89+
90+
assertPathValues()
91+
})
92+
93+
Context("that is positive", func() {
94+
BeforeEach(func() {
95+
inVersion, expVersion = "21", 21
96+
})
97+
98+
assertPathValues()
99+
})
100+
})
101+
})
102+
}
103+
})
104+
})
105+
}
106+
})
107+
108+
Context("with a path that has an unescaped colon and a key", func() {
109+
BeforeEach(func() {
110+
inPath, inKey = "secret:foo", "bar"
111+
expPath, expKey = "secret:foo", "bar"
112+
})
113+
114+
assertPathValues()
115+
})
116+
117+
Context("with a path that has an unescaped caret and a version", func() {
118+
BeforeEach(func() {
119+
inPath, inVersion = "secret^foo", "2"
120+
expPath, expVersion = "secret^foo", 2
121+
})
122+
})
123+
})
124+
})

vault/vault.go

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,6 @@ func (v *Vault) Curl(method string, path string, body []byte) (*http.Response, e
134134
// If there is nothing at that path, a nil *Secret will be returned, with no
135135
// error.
136136
func (v *Vault) Read(path string) (secret *Secret, err error) {
137-
path = Canonicalize(path)
138137
path, key, version := ParsePath(path)
139138

140139
secret = NewSecret()
@@ -190,11 +189,15 @@ func (v *Vault) List(path string) (paths []string, err error) {
190189

191190
// Write takes a Secret and writes it to the Vault at the specified path.
192191
func (v *Vault) Write(path string, s *Secret) error {
193-
path = Canonicalize(path)
194-
if strings.Contains(path, ":") {
192+
path, key, version := ParsePath(path)
193+
if key != "" {
195194
return fmt.Errorf("cannot write to paths in /path:key notation")
196195
}
197196

197+
if version != 0 {
198+
return fmt.Errorf("cannot write to paths in /path^version notation")
199+
}
200+
198201
if s.Empty() {
199202
return v.deleteIfPresent(path, DeleteOpts{})
200203
}
@@ -207,7 +210,7 @@ func (v *Vault) Write(path string, s *Secret) error {
207210
return err
208211
}
209212

210-
//errIfFolder returns an error with your provided message if the given path is a folder.
213+
// errIfFolder returns an error with your provided message if the given path is a folder.
211214
// Can also throw an error if contacting the backend failed, in which case that error
212215
// is returned.
213216
func (v *Vault) errIfFolder(path, message string, args ...interface{}) error {
@@ -316,8 +319,8 @@ func (v *Vault) verifySecretExists(path string) error {
316319
return err
317320
}
318321

319-
//DeleteTree recursively deletes the leaf nodes beneath the given root until
320-
//the root has no children, and then deletes that.
322+
// DeleteTree recursively deletes the leaf nodes beneath the given root until
323+
// the root has no children, and then deletes that.
321324
func (v *Vault) DeleteTree(root string, opts DeleteOpts) error {
322325
root = Canonicalize(root)
323326

@@ -486,13 +489,13 @@ func (v *Vault) deleteSpecificKey(path string) error {
486489
return v.Write(secretPath, secret)
487490
}
488491

489-
//DeleteVersions marks the given versions of the given secret as deleted for
492+
// DeleteVersions marks the given versions of the given secret as deleted for
490493
// a v2 backend or actually deletes it for a v1 backend.
491494
func (v *Vault) DeleteVersions(path string, versions []uint) error {
492495
return v.client.Delete(path, &vaultkv.KVDeleteOpts{Versions: versions, V1Destroy: true})
493496
}
494497

495-
//DestroyVersions irrevocably destroys the given versions of the given secret
498+
// DestroyVersions irrevocably destroys the given versions of the given secret
496499
func (v *Vault) DestroyVersions(path string, versions []uint) error {
497500
return v.client.Destroy(path, versions)
498501
}
@@ -530,7 +533,7 @@ func (v *Vault) Undelete(path string) error {
530533
return v.Client().Undelete(secret, []uint{uint(version)})
531534
}
532535

533-
//deleteIfPresent first checks to see if there is a Secret at the given path,
536+
// deleteIfPresent first checks to see if there is a Secret at the given path,
534537
// and if so, it deletes it. Otherwise, no error is thrown
535538
func (v *Vault) deleteIfPresent(path string, opts DeleteOpts) error {
536539
secretpath, _, _ := ParsePath(path)
@@ -699,7 +702,7 @@ func (v *Vault) Copy(oldpath, newpath string, opts MoveCopyOpts) error {
699702
return nil
700703
}
701704

702-
//MoveCopyTree will recursively copy all nodes from the root to the new location.
705+
// MoveCopyTree will recursively copy all nodes from the root to the new location.
703706
// This function will get confused about 'secret:key' syntax, so don't let those
704707
// get routed here - they don't make sense for a recursion anyway.
705708
func (v *Vault) MoveCopyTree(oldRoot, newRoot string, f func(string, string, MoveCopyOpts) error, opts MoveCopyOpts) error {

0 commit comments

Comments
 (0)