diff --git a/README.md b/README.md
index 1852580..99e3ff0 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,8 @@
# š Mycorrhiza Wiki
+**Mycorrhiza Wiki** is a lightweight file-system wiki engine that uses Git for keeping history. [Main wiki](https://mycorrhiza.wiki)
-[![Go Report Card](https://goreportcard.com/badge/github.com/bouncepaw/mycorrhiza)](https://goreportcard.com/report/github.com/bouncepaw/mycorrhiza)
-
-**Mycorrhiza Wiki** is a lightweight file-system wiki engine that uses Git for keeping history.
-
-[š Main wiki](https://mycorrhiza.wiki)
-
## Features
* **No database required.** Everything is stored in plain files. It makes installation super easy, and you can modify the content directly by yourself.
diff --git a/hyphae/files.go b/hyphae/files.go
index 88d0525..cae0113 100644
--- a/hyphae/files.go
+++ b/hyphae/files.go
@@ -6,7 +6,6 @@ import (
"path/filepath"
"github.com/bouncepaw/mycorrhiza/mimetype"
- "github.com/bouncepaw/mycorrhiza/util"
)
// Index finds all hypha files in the full `path` and saves them to the hypha storage.
@@ -20,7 +19,7 @@ func Index(path string) {
}(ch)
for h := range ch {
- // At this time it is safe to ignore the mutex, because there is only one worker.
+ // It's safe to ignore the mutex because there is a single worker right now.
if oh := ByName(h.Name); oh.Exists {
oh.MergeIn(h)
} else {
@@ -32,7 +31,9 @@ func Index(path string) {
log.Println("Indexed", Count(), "hyphae")
}
-// indexHelper finds all hypha files in the full `path` and sends them to the channel. Handling of duplicate entries and attachment and counting them is up to the caller.
+// indexHelper finds all hypha files in the full `path` and sends them to the
+// channel. Handling of duplicate entries and attachment and counting them is
+// up to the caller.
func indexHelper(path string, nestLevel uint, ch chan *Hypha) {
nodes, err := os.ReadDir(path)
if err != nil {
@@ -40,10 +41,10 @@ func indexHelper(path string, nestLevel uint, ch chan *Hypha) {
}
for _, node := range nodes {
- // If this hypha looks like it can be a hypha path, go deeper. Do not touch the .git and static folders for they have an administrative importance!
- if node.IsDir() &&
- util.IsCanonicalName(node.Name()) &&
- node.Name() != ".git" &&
+ // If this hypha looks like it can be a hypha path, go deeper. Do not
+ // touch the .git and static folders for they have an administrative
+ // importance!
+ if node.IsDir() && IsValidName(node.Name()) && node.Name() != ".git" &&
!(nestLevel == 0 && node.Name() == "static") {
indexHelper(filepath.Join(path, node.Name()), nestLevel+1, ch)
continue
diff --git a/hyphae/hyphae.go b/hyphae/hyphae.go
index d7a1875..e31788a 100644
--- a/hyphae/hyphae.go
+++ b/hyphae/hyphae.go
@@ -2,16 +2,31 @@
package hyphae
import (
- "github.com/bouncepaw/mycorrhiza/files"
"log"
"path/filepath"
"regexp"
+ "strings"
"sync"
+
+ "github.com/bouncepaw/mycorrhiza/files"
)
-// HyphaPattern is a pattern which all hyphae must match.
+// HyphaPattern is a pattern which all hyphae names must match.
var HyphaPattern = regexp.MustCompile(`[^?!:#@><*|"'&%{}]+`)
+// IsValidName checks for invalid characters and path traversals.
+func IsValidName(hyphaName string) bool {
+ if !HyphaPattern.MatchString(hyphaName) {
+ return false
+ }
+ for _, segment := range strings.Split(hyphaName, "/") {
+ if segment == ".git" || segment == ".." {
+ return false
+ }
+ }
+ return true
+}
+
// Hypha keeps vital information about a hypha
type Hypha struct {
sync.RWMutex
diff --git a/shroom/can.go b/shroom/can.go
index 114dc7c..7db6430 100644
--- a/shroom/can.go
+++ b/shroom/can.go
@@ -14,7 +14,7 @@ func canFactory(
dispatcher func(*hyphae.Hypha, *user.User, *l18n.Localizer) (string, string),
noRightsMsg string,
notExistsMsg string,
- careAboutExistence bool,
+ mustExist bool,
) func(*user.User, *hyphae.Hypha, *l18n.Localizer) (string, error) {
return func(u *user.User, h *hyphae.Hypha, lc *l18n.Localizer) (string, error) {
if !u.CanProceed(action) {
@@ -22,7 +22,7 @@ func canFactory(
return lc.Get("ui.act_no_rights"), errors.New(lc.Get(noRightsMsg))
}
- if careAboutExistence && !h.Exists {
+ if mustExist && !h.Exists {
rejectLogger(h, u, "does not exist")
return lc.Get("ui.act_notexist"), errors.New(lc.Get(notExistsMsg))
}
diff --git a/shroom/rename.go b/shroom/rename.go
index e7e792c..d68897d 100644
--- a/shroom/rename.go
+++ b/shroom/rename.go
@@ -23,7 +23,7 @@ func canRenameThisToThat(oh *hyphae.Hypha, nh *hyphae.Hypha, u *user.User, lc *l
return lc.Get("ui.rename_noname"), errors.New(lc.Get("ui.rename_noname_tip"))
}
- if !hyphae.HyphaPattern.MatchString(nh.Name) {
+ if !hyphae.IsValidName(nh.Name) {
rejectRenameLog(oh, u, fmt.Sprintf("new name ā%sā invalid", nh.Name))
return lc.Get("ui.rename_badname"), errors.New(lc.Get("ui.rename_badname_tip", &l18n.Replacements{"chars": "^?!:#@><*|\"\\'&%
"}))
}
diff --git a/shroom/upload.go b/shroom/upload.go
index 6e4ffe2..176eb49 100644
--- a/shroom/upload.go
+++ b/shroom/upload.go
@@ -72,8 +72,7 @@ func uploadHelp(h *hyphae.Hypha, hop *history.Op, ext string, data []byte, u *us
originalFullPath = &h.TextPath
originalText = "" // for backlink update
)
- // Reject if the path is outside the hyphae dir
- if !strings.HasPrefix(fullPath, files.HyphaeDir()) {
+ if !isValidPath(fullPath) || !hyphae.IsValidName(h.Name) {
err := errors.New("bad path")
return hop.WithErrAbort(err), err.Error()
}
@@ -110,3 +109,7 @@ func uploadHelp(h *hyphae.Hypha, hop *history.Op, ext string, data []byte, u *us
}
return hop.WithFiles(fullPath).WithUser(u).Apply(), ""
}
+
+func isValidPath(pathname string) bool {
+ return strings.HasPrefix(pathname, files.HyphaeDir())
+}
diff --git a/tools.go b/tools.go
index 784b3f6..e4f9bb6 100644
--- a/tools.go
+++ b/tools.go
@@ -1,3 +1,4 @@
+//go:build tools
// +build tools
package tools
diff --git a/user/net.go b/user/net.go
index b439cb9..831c722 100644
--- a/user/net.go
+++ b/user/net.go
@@ -44,7 +44,7 @@ func Register(username, password, group, source string, force bool) error {
username = util.CanonicalName(username)
switch {
- case !util.IsPossibleUsername(username):
+ case !IsValidUsername(username):
return fmt.Errorf("illegal username ā%sā", username)
case !ValidGroup(group):
return fmt.Errorf("invalid group ā%sā", group)
diff --git a/user/user.go b/user/user.go
index e0522bd..c277a5b 100644
--- a/user/user.go
+++ b/user/user.go
@@ -2,6 +2,8 @@ package user
import (
"net/http"
+ "regexp"
+ "strings"
"sync"
"time"
@@ -9,7 +11,9 @@ import (
"golang.org/x/crypto/bcrypt"
)
-// User is a user (duh).
+var usernamePattern = regexp.MustCompile(`[^?!:#@><*|"'&%{}/]+`)
+
+// User contains information about a given user required for identification.
type User struct {
// Name is a username. It must follow hypha naming rules.
Name string `json:"name"`
@@ -117,3 +121,22 @@ func (user *User) ShowLockMaybe(w http.ResponseWriter, rq *http.Request) bool {
}
return false
}
+
+// IsValidUsername checks if the given username is valid.
+func IsValidUsername(username string) bool {
+ return username != "anon" && username != "wikimind" &&
+ usernamePattern.MatchString(strings.TrimSpace(username)) &&
+ usernameIsWhiteListed(username)
+}
+
+func usernameIsWhiteListed(username string) bool {
+ if !cfg.UseWhiteList {
+ return true
+ }
+ for _, allowedUsername := range cfg.WhiteList {
+ if allowedUsername == username {
+ return true
+ }
+ }
+ return false
+}
diff --git a/util/util.go b/util/util.go
index 7528364..f9c45f2 100644
--- a/util/util.go
+++ b/util/util.go
@@ -6,7 +6,6 @@ import (
"github.com/bouncepaw/mycorrhiza/files"
"log"
"net/http"
- "regexp"
"strings"
"github.com/bouncepaw/mycomarkup/v2/util"
@@ -65,33 +64,6 @@ func CanonicalName(name string) string {
return util.CanonicalName(name)
}
-// hyphaPattern is a pattern which all hypha names must match.
-var hyphaPattern = regexp.MustCompile(`[^?!:#@><*|"'&%{}]+`)
-
-var usernamePattern = regexp.MustCompile(`[^?!:#@><*|"'&%{}/]+`)
-
-// IsCanonicalName checks if the `name` is canonical.
-func IsCanonicalName(name string) bool {
- return hyphaPattern.MatchString(name)
-}
-
-// IsPossibleUsername is true if the given username is ok. Same as IsCanonicalName, but cannot have / in it and cannot be equal to "anon" or "wikimind"
-func IsPossibleUsername(username string) bool {
- return username != "anon" && username != "wikimind" && usernameIsWhiteListed(username) && usernamePattern.MatchString(strings.TrimSpace(username))
-}
-
-func usernameIsWhiteListed(username string) bool {
- if !cfg.UseWhiteList {
- return true
- }
- for _, allowedUsername := range cfg.WhiteList {
- if allowedUsername == username {
- return true
- }
- }
- return false
-}
-
// HyphaNameFromRq extracts hypha name from http request. You have to also pass the action which is embedded in the url or several actions. For url /hypha/hypha, the action would be "hypha".
func HyphaNameFromRq(rq *http.Request, actions ...string) string {
p := rq.URL.Path