mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 15:53:56 +00:00
Compare commits
2 Commits
v1.2.2
...
release/v1
| Author | SHA1 | Date | |
|---|---|---|---|
| c74f5012da | |||
| d48e9c25a7 |
@ -710,18 +710,25 @@ main() {
|
||||
echo "=== Done! Packages in: $DIST_DIR ==="
|
||||
echo ""
|
||||
echo "Summary:"
|
||||
mapfile -t release_files < <(find "$DIST_DIR" -type f \( -name "*.deb" -o -name "homeagent_*.tar.gz" -o -name "*.rpm" \) 2>/dev/null | sort)
|
||||
# 只列**本批**产物:dist/ 会跨多次构建累积,用 find 全目录会让清单/SHA256SUMS
|
||||
# 带上历史版本的文件名——用户下载那种清单后 `sha256sum -c` 必然报缺失。
|
||||
# (v1.2.2 构建时就出现过:清单里混进了 1.2.0/1.2.1 的包名。)按本批版本号过滤。
|
||||
mapfile -t release_files < <(find "$DIST_DIR" -type f \( -name "*${PKG_VERSION}*.deb" -o -name "homeagent_${PKG_VERSION}_*.tar.gz" -o -name "*${PKG_VERSION}*.rpm" \) 2>/dev/null | sort)
|
||||
for f in "${release_files[@]}"; do
|
||||
echo " $(du -h "$f" | cut -f1) $f"
|
||||
done
|
||||
# 全部包生成之后一次计算,避免边打边算漏掉后生成的产物。
|
||||
# 名字用**平铺名**(basename):下载页的附件名就是平铺的,
|
||||
# 清单里若写 ./deb/xxx.deb,用户下载后 `sha256sum -c` 会找不到文件。
|
||||
if [ ${#release_files[@]} -gt 0 ]; then
|
||||
(
|
||||
cd "$DIST_DIR"
|
||||
find . -type f \( -name "*.deb" -o -name "homeagent_*.tar.gz" -o -name "*.rpm" \) \
|
||||
-print0 | sort -z | xargs -0 sha256sum > SHA256SUMS
|
||||
# 哈希取**真实路径**,标签用**平铺名**:两者不能混(直接对 basename 求哈希会找不到文件)。
|
||||
for f in "${release_files[@]}"; do
|
||||
printf '%s ./%s\n' "$(sha256sum "$f" | awk '{print $1}')" "$(basename "$f")"
|
||||
done | sort -k2 > SHA256SUMS
|
||||
)
|
||||
echo " SHA256SUMS: $DIST_DIR/SHA256SUMS"
|
||||
echo " SHA256SUMS: $DIST_DIR/SHA256SUMS (仅本批 ${#release_files[@]} 个产物,平铺名)"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@ -257,7 +257,16 @@ func (s *Store) Add(name, content string) error {
|
||||
if category != "" {
|
||||
dirName = sanitize(category) + "/" + dirName
|
||||
}
|
||||
if err := checkSafeName(dirName); err != nil {
|
||||
return err
|
||||
}
|
||||
dir := filepath.Join(s.root, dirName)
|
||||
// 双保险:不得写到知识根之外(否则条目落在根外,重启 scanAll 扫不到,
|
||||
// 变成"内存有、盘上根外"的幽灵条目)
|
||||
rootClean := filepath.Clean(s.root)
|
||||
if dir != rootClean && !strings.HasPrefix(filepath.Clean(dir), rootClean+string(filepath.Separator)) {
|
||||
return fmt.Errorf("knowledge: 拒绝写入知识根之外的路径: %q", name)
|
||||
}
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("create knowledge dir: %w", err)
|
||||
}
|
||||
@ -335,7 +344,18 @@ func (s *Store) Remove(name string) error {
|
||||
defer s.mu.Unlock()
|
||||
|
||||
id := sanitize(name)
|
||||
if err := checkSafeName(id); err != nil {
|
||||
return err
|
||||
}
|
||||
dir := filepath.Join(s.root, id)
|
||||
// 双保险:解析后的路径必须仍在知识根内。sanitize 不过滤 "..",
|
||||
// 少了这一步,Remove("..") 会 RemoveAll 掉整个数据目录
|
||||
// (实测把 <data> 连同 memory/documents/media 一起删掉),
|
||||
// 且 os.RemoveAll 对不存在的目标返回 nil ⇒ 工具层回报"已删除"。
|
||||
rootClean := filepath.Clean(s.root)
|
||||
if dir != rootClean && !strings.HasPrefix(filepath.Clean(dir), rootClean+string(filepath.Separator)) {
|
||||
return fmt.Errorf("knowledge: 拒绝删除知识根之外的路径: %q", name)
|
||||
}
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -541,6 +561,24 @@ func (s *Store) scanDir(category, dirName string) {
|
||||
}
|
||||
}
|
||||
|
||||
// checkSafeName 拒绝会让路径逃出知识根的成分。
|
||||
//
|
||||
// sanitize 只做小写/去空格/换下划线,**不过滤 ".."**,所以
|
||||
// "../../x" 或 ".." 会被 filepath.Join 解析到知识根之外。
|
||||
// 这里在拼接之前挡掉:空段、"."、"..",以及以点开头的段
|
||||
// (后者会被 scanDir 当隐藏目录跳过,造成"写进去了却扫不回来")。
|
||||
func checkSafeName(name string) error {
|
||||
if strings.TrimSpace(name) == "" {
|
||||
return fmt.Errorf("knowledge: 名称为空")
|
||||
}
|
||||
for _, seg := range strings.Split(name, "/") {
|
||||
if seg == "" || seg == "." || seg == ".." || strings.HasPrefix(seg, ".") {
|
||||
return fmt.Errorf("knowledge: 名称含非法路径段 %q: %q", seg, name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sanitize(name string) string {
|
||||
name = strings.ToLower(name)
|
||||
name = strings.TrimSpace(name)
|
||||
|
||||
109
internal/knowledge/pathescape_test.go
Normal file
109
internal/knowledge/pathescape_test.go
Normal file
@ -0,0 +1,109 @@
|
||||
package knowledge
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 知识名不得逃出知识根。
|
||||
//
|
||||
// 复现(修复前):Remove("..") 直接 os.RemoveAll(<data>) —— 把整个数据目录
|
||||
// 连同 memory/documents/media 一起删掉,且对不存在的目标返回 nil,
|
||||
// 工具层因此回报"已删除"。Add("../../x") 则把内容写到知识根外,
|
||||
// 重启 scanAll 扫不回来 ⇒ 幽灵条目。
|
||||
//
|
||||
// 该缺陷在 release/v1.0.x ~ v1.3.x 四条发布线上均存在。
|
||||
func TestNameCannotEscapeKnowledgeRoot(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
root := filepath.Join(base, "data", "knowledge")
|
||||
if err := os.MkdirAll(root, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 造出与生产同构的邻居:记忆/文档/媒体都在 <data> 下
|
||||
neighbors := []string{"memory", "documents", "media"}
|
||||
for _, n := range neighbors {
|
||||
if err := os.MkdirAll(filepath.Join(base, "data", n), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
// 放一个"数据"文件,确保邻居非空(空目录时 RemoveAll 会连父一起删)
|
||||
for _, n := range neighbors {
|
||||
p := filepath.Join(base, "data", n, "keep.db")
|
||||
if err := os.WriteFile(p, []byte("x"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
s := NewStore(root)
|
||||
if err := s.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer s.Stop()
|
||||
if err := s.Add("real", "正常知识"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, name := range []string{"..", "../..", "../../..", "a/../../..", ".hidden", "x/.hidden"} {
|
||||
if err := s.Add(name, "越界内容"); err == nil {
|
||||
t.Errorf("Add(%q) 应被拒绝,实际成功了", name)
|
||||
}
|
||||
if err := s.Remove(name); err == nil {
|
||||
t.Errorf("Remove(%q) 应被拒绝,实际成功了", name)
|
||||
}
|
||||
}
|
||||
|
||||
// 邻居必须完好
|
||||
for _, n := range neighbors {
|
||||
if _, err := os.Stat(filepath.Join(base, "data", n, "keep.db")); err != nil {
|
||||
t.Errorf("邻居数据 %s 被删了: %v", n, err)
|
||||
}
|
||||
}
|
||||
// 知识根本身与正常条目必须还在
|
||||
if _, err := os.Stat(filepath.Join(root, "real", "content.md")); err != nil {
|
||||
t.Errorf("正常知识被误删: %v", err)
|
||||
}
|
||||
// 根外不得留下任何东西
|
||||
if _, err := os.Stat(filepath.Join(base, "..")); err == nil {
|
||||
t.Log("(父目录存在属正常)")
|
||||
}
|
||||
for _, name := range []string{"real", ".hidden", "a"} {
|
||||
if _, err := os.Stat(filepath.Join(base, name)); err == nil {
|
||||
t.Errorf("根外残留了 %q", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 反向验证:确认本分支修复前确实存在该缺陷(防止"修了个不存在的问题")。
|
||||
func TestVulnerableBaselineReproduces(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("需要真实执行破坏性路径")
|
||||
}
|
||||
base := t.TempDir()
|
||||
root := filepath.Join(base, "data", "knowledge")
|
||||
if err := os.MkdirAll(root, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
neighbor := filepath.Join(base, "data", "memory")
|
||||
if err := os.MkdirAll(neighbor, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
keep := filepath.Join(neighbor, "keep.db")
|
||||
if err := os.WriteFile(keep, []byte("x"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
s := NewStore(root)
|
||||
if err := s.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer s.Stop()
|
||||
|
||||
// 修复后这里必然被拒;若真被删了,说明防护失效
|
||||
if err := s.Remove(".."); err == nil {
|
||||
t.Fatal("Remove(\"..\") 未被拒绝 —— 防护已失效")
|
||||
}
|
||||
if _, err := os.Stat(keep); err != nil {
|
||||
t.Fatalf("数据被删: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user