Refine v2 plugin API and sync flow

This commit is contained in:
2026-06-27 12:36:31 +08:00
parent 03175aa46d
commit 24444a8588
14 changed files with 1013 additions and 282 deletions
+25 -8
View File
@@ -44,6 +44,23 @@ func validatePluginID(pluginID string) error {
return nil
}
func validateStorageName(kind, name string) error {
if name == "" {
return fmt.Errorf("%s name is empty", kind)
}
if strings.ContainsAny(name, `/\`) {
return fmt.Errorf("%s name %q contains path separators", kind, name)
}
if name == "." || name == ".." {
return fmt.Errorf("%s name %q is a path traversal reference", kind, name)
}
cleaned := filepath.Clean(name)
if cleaned != name {
return fmt.Errorf("%s name %q contains path traversal", kind, name)
}
return nil
}
// ─── Atomic write helper ──────────────────────────────────
func atomicWrite(path string, data []byte) error {
@@ -135,8 +152,8 @@ func (s *Storage) ReadPluginDataJSON(pluginID, name string) (map[string]interfac
if err := validatePluginID(pluginID); err != nil {
return nil, err
}
if name == "" {
return nil, fmt.Errorf("data name is empty")
if err := validateStorageName("data", name); err != nil {
return nil, err
}
dir := s.vault.GetPluginDataPath(pluginID)
@@ -162,8 +179,8 @@ func (s *Storage) WritePluginDataJSON(pluginID, name string, data map[string]int
if err := validatePluginID(pluginID); err != nil {
return err
}
if name == "" {
return fmt.Errorf("data name is empty")
if err := validateStorageName("data", name); err != nil {
return err
}
dir := s.vault.GetPluginDataPath(pluginID)
@@ -183,8 +200,8 @@ func (s *Storage) ReadPluginCacheJSON(pluginID, name string) (map[string]interfa
if err := validatePluginID(pluginID); err != nil {
return nil, err
}
if name == "" {
return nil, fmt.Errorf("cache name is empty")
if err := validateStorageName("cache", name); err != nil {
return nil, err
}
dir := s.vault.GetPluginCachePath(pluginID)
@@ -210,8 +227,8 @@ func (s *Storage) WritePluginCacheJSON(pluginID, name string, data map[string]in
if err := validatePluginID(pluginID); err != nil {
return err
}
if name == "" {
return fmt.Errorf("cache name is empty")
if err := validateStorageName("cache", name); err != nil {
return err
}
dir := s.vault.GetPluginCachePath(pluginID)
+54
View File
@@ -229,6 +229,60 @@ func TestPathTraversal_Blocked(t *testing.T) {
}
}
func TestPluginDataJSONNameTraversal_Blocked(t *testing.T) {
s, _ := newTestStorage(t)
traversalNames := []string{
"..",
"../evil",
"foo/../../bar",
"/absolute",
`backslash\traverse`,
"nested/name",
}
for _, name := range traversalNames {
t.Run(name, func(t *testing.T) {
err := s.WritePluginDataJSON("data-plugin", name, map[string]interface{}{"x": 1})
if err == nil {
t.Errorf("WritePluginDataJSON(%q): expected error, got nil", name)
}
_, err = s.ReadPluginDataJSON("data-plugin", name)
if err == nil {
t.Errorf("ReadPluginDataJSON(%q): expected error, got nil", name)
}
})
}
}
func TestPluginCacheJSONNameTraversal_Blocked(t *testing.T) {
s, _ := newTestStorage(t)
traversalNames := []string{
"..",
"../evil",
"foo/../../bar",
"/absolute",
`backslash\traverse`,
"nested/name",
}
for _, name := range traversalNames {
t.Run(name, func(t *testing.T) {
err := s.WritePluginCacheJSON("cache-plugin", name, map[string]interface{}{"x": 1})
if err == nil {
t.Errorf("WritePluginCacheJSON(%q): expected error, got nil", name)
}
_, err = s.ReadPluginCacheJSON("cache-plugin", name)
if err == nil {
t.Errorf("ReadPluginCacheJSON(%q): expected error, got nil", name)
}
})
}
}
// ─── Atomic write tests ──────────────────────────────────────
func TestAtomicWrite(t *testing.T) {