Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion internal/server/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"strings"
Expand Down Expand Up @@ -481,6 +482,11 @@ func (h *Handler) GetStatic(c echo.Context) error {

absolutePath := filepath.Join(h.setting.Static, relativePath)

// Serve index.html for directory requests
if info, statErr := os.Stat(absolutePath); statErr == nil && info.IsDir() {
absolutePath = filepath.Join(absolutePath, "index.html")
}

return c.File(absolutePath)
}

Expand Down Expand Up @@ -519,7 +525,8 @@ func paramPath(c echo.Context, param string) (string, error) {
return "", fmt.Errorf("path unescape: %w", err)
}

cleanPath := filepath.Clean("/" + urlPath)
// Use path.Clean (not filepath.Clean) for URL paths - URLs always use forward slashes
cleanPath := path.Clean("/" + urlPath)
if cleanPath != "/"+urlPath {
return "", ErrInvalidPath
}
Expand Down
105 changes: 105 additions & 0 deletions internal/server/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,13 @@ func TestGetStatic(t *testing.T) {
err = os.WriteFile(indexPath, []byte("<html>test</html>"), 0644)
require.NoError(t, err)

// Create nested directory with file
scriptsDir := filepath.Join(staticDir, "scripts")
err = os.MkdirAll(scriptsDir, 0755)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(scriptsDir, "ocap.js"), []byte("// test"), 0644)
require.NoError(t, err)

hdlr := Handler{
setting: Setting{Static: staticDir},
}
Expand All @@ -929,6 +936,32 @@ func TestGetStatic(t *testing.T) {
assert.NoError(t, err)
})

t.Run("root path serves index.html", func(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetParamNames("*")
c.SetParamValues("") // Empty param for root path

err := hdlr.GetStatic(c)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), "<html>test</html>")
})

t.Run("nested path with forward slashes", func(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/scripts/ocap.js", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetParamNames("*")
c.SetParamValues("scripts/ocap.js") // Forward slashes in path

err := hdlr.GetStatic(c)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), "// test")
})

t.Run("path traversal blocked", func(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/../../../etc/passwd", nil)
Expand All @@ -942,6 +975,78 @@ func TestGetStatic(t *testing.T) {
})
}

func TestParamPath(t *testing.T) {
tests := []struct {
name string
param string
wantPath string
wantError bool
}{
{
name: "empty path returns root",
param: "",
wantPath: "/",
wantError: false,
},
{
name: "simple filename",
param: "index.html",
wantPath: "/index.html",
wantError: false,
},
{
name: "nested path with forward slashes",
param: "scripts/ocap.js",
wantPath: "/scripts/ocap.js",
wantError: false,
},
{
name: "deeply nested path",
param: "assets/images/logo.png",
wantPath: "/assets/images/logo.png",
wantError: false,
},
{
name: "path traversal blocked",
param: "../../../etc/passwd",
wantPath: "",
wantError: true,
},
{
name: "double slash blocked",
param: "foo//bar",
wantPath: "",
wantError: true,
},
{
name: "dot segment blocked",
param: "foo/../bar",
wantPath: "",
wantError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/"+tt.param, nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetParamNames("*")
c.SetParamValues(tt.param)

got, err := paramPath(c, "*")

if tt.wantError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.wantPath, got)
}
})
}
}

func TestCacheControl(t *testing.T) {
hdlr := Handler{}

Expand Down
Loading