mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-10 18:56:55 +02:00
Finish the file API traversal fix by rooting both GET and SET operations at the
actual file-type directory instead of the process working directory. This blocks
`..` escapes from `config/` and `config/middlewares/` while preserving valid
in-root reads and writes.
Also harden the optional unauthenticated local API listener so it only starts on
loopback addresses (`localhost`, `127.0.0.1`, `::1`). This preserves same-host
automation while preventing accidental exposure on wildcard, LAN, bridge, or
public interfaces.
Add regression tests for blocked traversal on GET and SET, valid in-root writes,
and loopback-only local API address validation. Fix an unrelated config test
cleanup panic so the touched package verification can run cleanly.
Constraint: `GODOXY_LOCAL_API_ADDR` is documented for local automation and must remain usable without adding a new auth flow
Constraint: File API behavior must keep valid config/provider/middleware edits working while blocking path escapes
Rejected: Mirror the previous GET `OpenInRoot(".", ...)` approach in SET | still allows escapes from `config/` to sibling paths under the working directory
Rejected: Keep unauthenticated non-loopback local API binds and document the risk | preserves a high-severity pre-auth network exposure
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Treat `LOCAL_API_ADDR` as same-host only; if non-loopback unauthenticated access is ever needed, gate it behind a separately named explicit insecure opt-in
Tested: `go test -count=1 -ldflags='-checklinkname=0' ./internal/api/v1/file -run 'Test(Get|Set)_PathTraversalBlocked' -v`
Tested: `go test -count=1 -ldflags='-checklinkname=0' ./internal/config -run '^TestValidateLocalAPIAddr$|^TestRouteValidateInboundMTLSProfile$' -v`
Tested: `go test -count=1 -ldflags='-checklinkname=0' ./internal/api/... ./internal/config/...`
Not-tested: End-to-end runtime verification of fsnotify reload behavior after a valid in-root provider edit
88 lines
2.0 KiB
Go
88 lines
2.0 KiB
Go
package fileapi_test
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const validProviderYAML = `app:
|
|
host: attacker.com
|
|
port: 443
|
|
scheme: https
|
|
`
|
|
|
|
func TestSet_PathTraversalBlocked(t *testing.T) {
|
|
root := setupFileAPITestRoot(t)
|
|
r := newFileContentRouter()
|
|
|
|
t.Run("write_in_root_file", func(t *testing.T) {
|
|
req := httptest.NewRequest(
|
|
http.MethodPut,
|
|
"/api/v1/file/content?type=provider&filename=providers.yml",
|
|
strings.NewReader(validProviderYAML),
|
|
)
|
|
req.Header.Set("Content-Type", "text/plain")
|
|
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
content, err := os.ReadFile(filepath.Join(root, "config", "providers.yml"))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, validProviderYAML, string(content))
|
|
})
|
|
|
|
const originalContent = "do not overwrite\n"
|
|
require.NoError(t, os.WriteFile(filepath.Join(root, "secret.yml"), []byte(originalContent), 0o644))
|
|
|
|
tests := []struct {
|
|
name string
|
|
filename string
|
|
queryEscaped bool
|
|
}{
|
|
{
|
|
name: "dotdot_traversal_to_sibling_file",
|
|
filename: "../secret.yml",
|
|
},
|
|
{
|
|
name: "url_encoded_dotdot_traversal_to_sibling_file",
|
|
filename: "../secret.yml",
|
|
queryEscaped: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
filename := tt.filename
|
|
if tt.queryEscaped {
|
|
filename = url.QueryEscape(filename)
|
|
}
|
|
|
|
req := httptest.NewRequest(
|
|
http.MethodPut,
|
|
"/api/v1/file/content?type=provider&filename="+filename,
|
|
strings.NewReader(validProviderYAML),
|
|
)
|
|
req.Header.Set("Content-Type", "text/plain")
|
|
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.NotEqual(t, http.StatusOK, w.Code)
|
|
|
|
content, err := os.ReadFile(filepath.Join(root, "secret.yml"))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, originalContent, string(content))
|
|
})
|
|
}
|
|
}
|