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) A screenshot of mycorrhiza.wiki's home page in the Safari browser -[![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