security(skills): validate GitHub repo format and limit download size

Add regex validation for 'owner/repo' format to prevent URL injection
in InstallFromGitHub. Add 5 MB LimitReader on skill file downloads
to prevent memory exhaustion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
admin-mf 2026-03-06 00:00:27 -06:00
parent b391e649d2
commit 48b08110de

View file

@ -7,12 +7,17 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"time" "time"
"github.com/sipeed/picoclaw/pkg/fileutil" "github.com/sipeed/picoclaw/pkg/fileutil"
"github.com/sipeed/picoclaw/pkg/utils" "github.com/sipeed/picoclaw/pkg/utils"
) )
const maxSkillFileSize int64 = 5 << 20 // 5 MB — SKILL.md files should never approach this
var repoPattern = regexp.MustCompile(`^[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+$`)
type SkillInstaller struct { type SkillInstaller struct {
workspace string workspace string
} }
@ -24,6 +29,10 @@ func NewSkillInstaller(workspace string) *SkillInstaller {
} }
func (si *SkillInstaller) InstallFromGitHub(ctx context.Context, repo string) error { func (si *SkillInstaller) InstallFromGitHub(ctx context.Context, repo string) error {
if !repoPattern.MatchString(repo) {
return fmt.Errorf("invalid repository format %q: must be 'owner/repo'", repo)
}
skillDir := filepath.Join(si.workspace, "skills", filepath.Base(repo)) skillDir := filepath.Join(si.workspace, "skills", filepath.Base(repo))
if _, err := os.Stat(skillDir); err == nil { if _, err := os.Stat(skillDir); err == nil {
@ -48,7 +57,7 @@ func (si *SkillInstaller) InstallFromGitHub(ctx context.Context, repo string) er
return fmt.Errorf("failed to fetch skill: HTTP %d", resp.StatusCode) return fmt.Errorf("failed to fetch skill: HTTP %d", resp.StatusCode)
} }
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(io.LimitReader(resp.Body, maxSkillFileSize))
if err != nil { if err != nil {
return fmt.Errorf("failed to read response: %w", err) return fmt.Errorf("failed to read response: %w", err)
} }