mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-19 08:15:12 +00:00
fix(skills): normalize backslash in ZIP entry names for Windows compatibility (#1103)
* fix(skills): normalize backslash in ZIP entry names for Windows compatibility ZIP archives created on Windows may use backslash (\) as path separator instead of forward slash (/). When extracted on Linux, this causes files like 'scripts\search.py' to be created as a single flat file instead of the proper 'scripts/search.py' directory structure. The ZIP specification (PKZIP APPNOTE) requires forward slashes, but many Windows tools produce non-conforming archives. This adds a strings.ReplaceAll backslash normalization step during skill ZIP extraction to handle this edge case. * test(skills): cover backslash zip extraction --------- Co-authored-by: buidackim <221586880+buidackim@users.noreply.github.com> Co-authored-by: Mỹ Ngân <goclaw-maintain@users.noreply.github.com>
This commit is contained in:
2 files changed
+49
-1
No files matched your search
@@ -233,7 +233,13 @@ func (h *SkillsHandler) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Skip macOS/system artifacts
|
||||
// Normalize Windows backslash separators in ZIP entries.
|
||||
// ZIPs created on Windows may use `\` instead of `/` in paths,
|
||||
// causing files like "scripts\search.py" to be extracted as a
|
||||
// single flat file instead of scripts/search.py on Linux.
|
||||
entryName = strings.ReplaceAll(entryName, "\\", "/")
|
||||
// Skip macOS/system artifacts after separator normalization so
|
||||
// entries like "__MACOSX\\._SKILL.md" are filtered too.
|
||||
if skills.IsSystemArtifact(entryName) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
@@ -1422,6 +1423,47 @@ func TestHandleUpload_ResponseIncludesIsNew(t *testing.T) {
|
||||
|
||||
// --- Security Guard Tests ---
|
||||
|
||||
func TestHandleUpload_NormalizesBackslashZipEntryPaths(t *testing.T) {
|
||||
handler, skillStore, ctx, _ := newTestUploadHandler(t)
|
||||
stubUploadDepFns(t,
|
||||
func(context.Context, *skills.SkillManifest, []string) (*skills.InstallResult, error) {
|
||||
return nil, nil
|
||||
},
|
||||
func(*skills.SkillManifest) (bool, []string) { return true, nil },
|
||||
)
|
||||
|
||||
req := newZipUploadRequest(t, ctx, map[string]string{
|
||||
"SKILL.md": skillMarkdown("Windows ZIP Skill", "windows-zip-skill"),
|
||||
"scripts\\search.py": "print('ok')\n",
|
||||
})
|
||||
w := httptest.NewRecorder()
|
||||
handler.handleUpload(w, req)
|
||||
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("status = %d, body = %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
info, ok := skillStore.GetSkillByID(ctx, uuid.MustParse(resp.ID))
|
||||
if !ok {
|
||||
t.Fatal("GetSkillByID returned !ok")
|
||||
}
|
||||
|
||||
wantPath := filepath.Join(info.BaseDir, "scripts", "search.py")
|
||||
if _, err := os.Stat(wantPath); err != nil {
|
||||
t.Fatalf("expected normalized path %s to exist: %v", wantPath, err)
|
||||
}
|
||||
flatPath := filepath.Join(info.BaseDir, `scripts\search.py`)
|
||||
if _, err := os.Stat(flatPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("flat backslash path %s exists or stat failed unexpectedly: %v", flatPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleUpload_MaliciousContent_CurlPipeBash_Rejected(t *testing.T) {
|
||||
handler, _, ctx, _ := newTestUploadHandler(t)
|
||||
stubUploadDepFns(t,
|
||||
|
||||
Reference in new issue
Block a user