mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
115 lines
3.0 KiB
Go
115 lines
3.0 KiB
Go
// Pass 4 — asset handler tests.
|
|||
|
|
//
|
||
|
|
// Locks in the Pass 2 + Pass 3 hardening: only manifest-declared files are
|
||
|
|
// served, path traversal is rejected, prefix-without-separator escapes are
|
||
|
|
// rejected via the filepath.Rel check.
|
||
|
|
package plugin
|
||
|
|
|
||
|
|
import (
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"testing"
|
||
|
|
)
|
||
|
|
|
||
|
|
func newAssetTestInstance(t *testing.T, declaredAssets []string) (*Registry, *Instance, string) {
|
||
|
|
t.Helper()
|
||
|
|
dir := t.TempDir()
|
||
|
|
// The handler uses filepath.Dir(inst.WASMPath) as pluginDir.
|
||
|
|
wasmPath := filepath.Join(dir, "main.wasm")
|
||
|
|
if err := os.WriteFile(wasmPath, []byte("\x00asm"), 0o644); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
tabs := make([]UITab, len(declaredAssets))
|
||
|
|
for i, a := range declaredAssets {
|
||
|
|
tabs[i] = UITab{ID: "tab", Asset: a}
|
||
|
|
// Create the file under the plugin dir so ServeFile can find it.
|
||
|
|
full := filepath.Join(dir, a)
|
||
|
|
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := os.WriteFile(full, []byte("hello: "+a), 0o644); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
r := &Registry{}
|
||
|
|
inst := &Instance{
|
||
|
|
ID: 1,
|
||
|
|
WASMPath: wasmPath,
|
||
|
|
Manifest: &Manifest{
|
||
|
|
Name: "ui-test",
|
||
|
|
Version: "0.1.0",
|
||
|
|
Entrypoint: "main.wasm",
|
||
|
|
Permissions: []string{string(CapUI)},
|
||
|
|
UI: UISpec{Tabs: tabs},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
return r, inst, dir
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAssetHandlerServesAllowedFile(t *testing.T) {
|
||
|
|
r, inst, _ := newAssetTestInstance(t, []string{"index.html"})
|
||
|
|
h := r.AssetHandler(inst)
|
||
|
|
|
||
|
|
req := httptest.NewRequest("GET", "/index.html", nil)
|
||
|
|
w := httptest.NewRecorder()
|
||
|
|
h.ServeHTTP(w, req)
|
||
|
|
|
||
|
|
if w.Code != http.StatusOK {
|
||
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
||
|
|
}
|
||
|
|
if got := w.Body.String(); got != "hello: index.html" {
|
||
|
|
t.Fatalf("unexpected body: %q", got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAssetHandlerRejectsUnlistedFile(t *testing.T) {
|
||
|
|
r, inst, dir := newAssetTestInstance(t, []string{"index.html"})
|
||
|
|
// File EXISTS in the plugin dir but is not declared in the manifest.
|
||
|
|
if err := os.WriteFile(filepath.Join(dir, "secret.txt"), []byte("nope"), 0o644); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
h := r.AssetHandler(inst)
|
||
|
|
|
||
|
|
req := httptest.NewRequest("GET", "/secret.txt", nil)
|
||
|
|
w := httptest.NewRecorder()
|
||
|
|
h.ServeHTTP(w, req)
|
||
|
|
|
||
|
|
if w.Code != http.StatusNotFound {
|
||
|
|
t.Fatalf("expected 404 for undeclared file, got %d", w.Code)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAssetHandlerRejectsTraversal(t *testing.T) {
|
||
|
|
r, inst, _ := newAssetTestInstance(t, []string{"index.html"})
|
||
|
|
h := r.AssetHandler(inst)
|
||
|
|
|
||
|
|
cases := []string{
|
||
|
|
"/../../../etc/passwd",
|
||
|
|
"/..%2Fpasswd",
|
||
|
|
"/etc/passwd",
|
||
|
|
}
|
||
|
|
for _, p := range cases {
|
||
|
|
req := httptest.NewRequest("GET", p, nil)
|
||
|
|
w := httptest.NewRecorder()
|
||
|
|
h.ServeHTTP(w, req)
|
||
|
|
if w.Code == http.StatusOK {
|
||
|
|
t.Errorf("traversal path %q should not have returned 200", p)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAssetHandlerServesNestedAsset(t *testing.T) {
|
||
|
|
r, inst, _ := newAssetTestInstance(t, []string{"assets/app.js"})
|
||
|
|
h := r.AssetHandler(inst)
|
||
|
|
|
||
|
|
req := httptest.NewRequest("GET", "/assets/app.js", nil)
|
||
|
|
w := httptest.NewRecorder()
|
||
|
|
h.ServeHTTP(w, req)
|
||
|
|
|
||
|
|
if w.Code != http.StatusOK {
|
||
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
||
|
|
}
|
||
|
|
}
|