mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
The previous filepath.IsLocal-up-front pattern in safeJoin/safeJoin-equivalents turned out not to satisfy CodeQL's go/path-injection rule — the post-validation filepath.Join still constructs the joined string from tainted input, so the analyser conservatively assumes the os.* sink that consumes it is tainted too. Only one of 34 alerts closed on the previous attempt. Switch to *os.Root (Go 1.24+, available on the project's 1.26.3 toolchain). The Go runtime guarantees that operations on a Root cannot escape the anchored directory regardless of what's in the relative path, and CodeQL has a built-in model that recognises *os.Root.* methods as path-traversal sanitisers. Result: every os.* sink in the datastore, marge, recorder, mirror parity-mismatch writer, and docs handler is now reached only via a *os.Root, which closes the rule-level alerts cleanly. Changes per file: * pkg/service/datastore/datastore.go — Adds a `root *os.Root` to DataStore, lazily opened at first use (after MkdirAll-ing baseDir) and closed by a new `(*DataStore).Close()`. Adds package-private helpers (rootStat / rootReadFile / rootWriteFile / rootMkdirAll / rootRemove / rootRemoveAll / rootRename / rootReadDir / rootOpen / rootExists) plus three exported wrappers (ReadDirUnderBase, MkdirAllUnderBase, WriteFileUnderBase) for the cross-package marge / handlers callers. Every os.* call that previously consumed safeJoin output now goes through these helpers. The post-join belt-and-suspenders prefix check inside safeJoin is preserved as a defence-in-depth fallback. * pkg/service/marge/marge.go — Replaces the five `os.ReadDir(devicesDir)` call sites with `ds.ReadDirUnderBase(...)` so the datastore's root enforces containment. * pkg/service/proxy/recorder.go — Mirrors the datastore pattern with its own `root *os.Root` anchored at Recorder.BaseDir, lazily opened. New helpers convert the eight existing `os.*` sites that consume sessionID / relPath / sanitizedSegments inputs. The earlier safeJoin (filepath.IsLocal pre-check) stays in place as the same belt-and-suspenders guard. * pkg/service/handlers/handlers_docs.go — Opens a *os.Root at "docs" via sync.Once and reads file content (and SUMMARY.md sidebar) through it. Removes the prior filepath.IsLocal pre-check; the runtime now guarantees containment. * pkg/service/handlers/mirror_middleware.go — Routes the parity-mismatch JSON write through `s.ds.WriteFileUnderBase` so the datastore's root performs the path-traversal sanitiser. Behavioural fix: *os.File.ReadDir(-1) returns directory entries in filesystem order, but os.ReadDir is documented to sort by name and at least one regression test (handlers.TestMargeAccountFullExcludesEmptyAmazonSource) depends on the sorted contract. Both rootReadDir helpers explicitly sort by name to match. All test suites pass for the touched packages; the unrelated TestDocsConsistency failure about untracked working-tree docs is pre-existing. golangci-lint reports 0 issues. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
159 lines
5.6 KiB
Go
159 lines
5.6 KiB
Go
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"html"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/russross/blackfriday/v2"
|
|
)
|
|
|
|
var (
|
|
docsRootOnce sync.Once
|
|
docsRoot *os.Root
|
|
)
|
|
|
|
// docsRootHandle returns a *os.Root anchored at the on-disk "docs" directory.
|
|
// All file reads from HandleDocs go through it so the Go runtime guarantees
|
|
// containment regardless of what HTTP path the caller sends — CodeQL also
|
|
// recognises *os.Root.* as a path-traversal sanitiser.
|
|
func docsRootHandle() *os.Root {
|
|
docsRootOnce.Do(func() {
|
|
r, err := os.OpenRoot("docs")
|
|
if err != nil {
|
|
// Fall back to nil; HandleDocs degrades to 404 below.
|
|
return
|
|
}
|
|
|
|
docsRoot = r
|
|
})
|
|
|
|
return docsRoot
|
|
}
|
|
|
|
// HandleDocs returns a handler for serving documentation files as HTML.
|
|
func (s *Server) HandleDocs(w http.ResponseWriter, r *http.Request) {
|
|
path := strings.TrimPrefix(r.URL.Path, "/docs")
|
|
|
|
path = strings.TrimPrefix(path, "/")
|
|
if path == "" {
|
|
path = "guides/SURVIVAL-GUIDE.md"
|
|
}
|
|
|
|
root := docsRootHandle()
|
|
if root == nil {
|
|
http.Error(w, "Documentation not available", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
content, err := root.ReadFile(path)
|
|
if err != nil {
|
|
// *os.Root.ReadFile rejects absolute paths and ".." segments at the
|
|
// runtime level, so any failure here is either "not found" or
|
|
// "traversal attempt blocked" — both 404 from the user's view.
|
|
http.Error(w, "File not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// Load sidebar (SUMMARY.md)
|
|
summaryContent, _ := root.ReadFile("SUMMARY.md")
|
|
|
|
sidebar := ""
|
|
if len(summaryContent) > 0 {
|
|
// Render summary to HTML
|
|
sidebar = string(blackfriday.Run(summaryContent))
|
|
// Adjust links in sidebar to be relative to /docs/
|
|
sidebar = strings.ReplaceAll(sidebar, "href=\"guides/", "href=\"/docs/guides/")
|
|
sidebar = strings.ReplaceAll(sidebar, "href=\"reference/", "href=\"/docs/reference/")
|
|
sidebar = strings.ReplaceAll(sidebar, "href=\"analysis/", "href=\"/docs/analysis/")
|
|
// Fix relative links that don't have a directory prefix (root docs)
|
|
// We look for href="filename.md" and replace with href="/docs/filename.md"
|
|
// This avoids manual listing of every file.
|
|
sidebar = s.fixSidebarLinks(sidebar)
|
|
}
|
|
|
|
// Render markdown to HTML
|
|
output := blackfriday.Run(content)
|
|
|
|
// Wrap in a documentation template with sidebar. The user-supplied path
|
|
// is escaped before interpolation; the sidebar and rendered markdown
|
|
// output are server-controlled (loaded from local files) and may
|
|
// legitimately contain HTML.
|
|
titleSafe := html.EscapeString(path)
|
|
|
|
w.Header().Set("Content-Type", "text/html")
|
|
_, _ = fmt.Fprintf(w, `<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>%s - Bose SoundTouch Toolkit Docs</title>
|
|
<link rel="icon" href="/web/img/favicon-braille.svg" type="image/svg+xml">
|
|
<link rel="stylesheet" href="/web/css/style.css">
|
|
<style>
|
|
body { margin: 0; padding: 0; display: flex; font-family: sans-serif; height: 100vh; overflow: hidden; }
|
|
.sidebar { width: 300px; background: #f8f9fa; border-right: 1px solid #dee2e6; padding: 20px; overflow-y: auto; flex-shrink: 0; }
|
|
.content-area { flex-grow: 1; overflow-y: auto; padding: 40px; }
|
|
.markdown-body { max-width: 800px; margin: 0 auto; line-height: 1.6; color: #333; }
|
|
h1, h2, h3 { color: #2196F3; }
|
|
pre { background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; }
|
|
code { font-family: monospace; background: #eee; padding: 2px 4px; border-radius: 3px; }
|
|
pre code { background: none; padding: 0; }
|
|
a { color: #2196F3; text-decoration: none; }
|
|
a:hover { text-decoration: underline; }
|
|
.back-link { margin-bottom: 20px; display: block; font-weight: bold; }
|
|
.sidebar h2 { font-size: 1.1em; margin-top: 20px; color: #666; text-transform: uppercase; letter-spacing: 1px; }
|
|
.sidebar ul { list-style: none; padding: 0; }
|
|
.sidebar li { margin-bottom: 8px; }
|
|
.sidebar a { color: #444; font-size: 0.95em; }
|
|
.sidebar a:hover { color: #2196F3; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="sidebar">
|
|
<a href="/" class="back-link">← Back to Toolkit</a>
|
|
%s
|
|
</div>
|
|
<div class="content-area">
|
|
<div class="markdown-body">
|
|
%s
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>`, titleSafe, sidebar, output)
|
|
}
|
|
|
|
// fixSidebarLinks ensures that relative links in the SUMMARY.md (sidebar)
|
|
// are correctly prefixed with /docs/ for the web UI.
|
|
func (s *Server) fixSidebarLinks(sidebar string) string {
|
|
// Root links like [Label](file.md) become href="file.md"
|
|
// We want href="/docs/file.md", but only if it doesn't already start with /docs/
|
|
// and isn't an external link.
|
|
// Since blackfriday renders [Label](file.md) as <a href="file.md">
|
|
|
|
// A simple but effective way is to use a regex or just check for common patterns.
|
|
// We already handled subdirectories. Now we handle files in the root of docs/
|
|
|
|
// We'll look for href="filename.md" where filename doesn't contain a slash
|
|
// and isn't already prefixed.
|
|
|
|
// Since we know our doc files always end in .md, we can look for that.
|
|
lines := strings.Split(sidebar, "\n")
|
|
for i, line := range lines {
|
|
if strings.Contains(line, "href=\"") && !strings.Contains(line, "href=\"/docs/") && !strings.Contains(line, "://") {
|
|
// Extract filename
|
|
start := strings.Index(line, "href=\"") + 6
|
|
end := strings.Index(line[start:], "\"") + start
|
|
filename := line[start:end]
|
|
|
|
if strings.HasSuffix(filename, ".md") && !strings.Contains(filename, "/") {
|
|
lines[i] = strings.ReplaceAll(line, "href=\""+filename+"\"", "href=\"/docs/"+filename+"\"")
|
|
}
|
|
}
|
|
}
|
|
|
|
return strings.Join(lines, "\n")
|
|
}
|