Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Code

395 commit(s) on main

update readme Keenan Nemetz · 57m ago 32290114
history blame raw 2.3 KiB
1package pages
2
3import (
4 "path"
5 "strings"
6 "testing"
7)
8
9// FuzzCleanPath drives the function that turns a request path into a path inside a
10// published tree.
11//
12// What it must never do is hand back a path that climbs. The tree lookup itself
13// cannot escape — a tree is a map of names, not a filesystem — but the result is
14// also used to build links, cache keys, and the path in a log line, and a value
15// with `..` in it is wrong in all three.
16//
17// dot segments are resolved rather than refused (`../../etc/passwd` becomes
18// `etc/passwd`), which is why the properties are about the result and not about
19// rejecting the input.
20func FuzzCleanPath(f *testing.F) {
21 for _, seed := range []string{
22 "index.html",
23 "docs/getting-started.md",
24 "a/b/c/d.txt",
25 "../../etc/passwd",
26 "a/../../b",
27 "./a/./b",
28 "a//b",
29 "/leading/slash",
30 "trailing/slash/",
31 "%2e%2e%2fetc%2fpasswd",
32 "a\x00b",
33 `a\b`,
34 "..",
35 ".",
36 "/",
37 "",
38 "a/b/../../../c",
39 "....//....//x",
40 } {
41 f.Add(seed)
42 }
43 f.Fuzz(func(t *testing.T, requestPath string) {
44 treePath, ok := cleanPath(requestPath)
45 if !ok {
46 return
47 }
48 if strings.HasPrefix(treePath, "/") || strings.HasSuffix(treePath, "/") {
49 t.Fatalf("cleanPath(%q) = %q, which is not a relative tree path", requestPath, treePath)
50 }
51 if strings.ContainsAny(treePath, "\\\x00") {
52 t.Fatalf("cleanPath(%q) = %q, which holds a separator the tree lookup cannot take", requestPath, treePath)
53 }
54 for _, segment := range strings.Split(treePath, "/") {
55 if treePath == "" {
56 break
57 }
58 if segment == "" || segment == "." || segment == ".." {
59 t.Fatalf("cleanPath(%q) = %q, which holds the dot segment %q", requestPath, treePath, segment)
60 }
61 }
62 // The result joined to a root stays under that root. This is the property
63 // the segment check exists to guarantee, stated the way the caller uses it.
64 joined := path.Join("/siteroot", treePath)
65 if joined != "/siteroot" && !strings.HasPrefix(joined, "/siteroot/") {
66 t.Fatalf("cleanPath(%q) = %q, which resolves to %q", requestPath, treePath, joined)
67 }
68 // Cleaning a cleaned path must not move it, or two requests for the same
69 // page would produce two cache keys.
70 again, ok := cleanPath(treePath)
71 if !ok || again != treePath {
72 t.Fatalf("cleanPath(%q) = %q, but cleaning that gives %q, %v", requestPath, treePath, again, ok)
73 }
74 })
75}
76