Files
Agent-System-Backend/internal/tools/ooxmlgate_test.go
weichen a2f4978be8 Detect Word packages from OPC metadata
Why: The registration gate classified Word packages from a hard-coded ZIP member path, allowing alternate main-part paths to bypass the gate and rejecting unrelated ZIP archives.

Evidence: Focused OPC registration tests; go test -count=1 ./internal/tools; go test -count=1 ./...; go test -race -count=1 ./internal/tools ./internal/runmgr; go vet ./...; go build ./...; git diff --check.

Impact: Resolve package-level officeDocument targets safely, classify all conventional Word main content types through OPC defaults or overrides, and fail closed on malformed, ambiguous, external, or traversing metadata while preserving non-Word artifacts.

Refs: Task 10; /tmp/task10-backend-final-closure-last.md
2026-10-15 22:46:21 +00:00

553 lines
25 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package tools
import (
"archive/zip"
"bytes"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
// zipOf 按序打包字符串部件(保持插入序便于断言)。
func zipOf(t *testing.T, files map[string]string) []byte {
t.Helper()
buf := new(bytes.Buffer)
w := zip.NewWriter(buf)
for name, body := range files {
f, err := w.Create(name)
if err != nil {
t.Fatal(err)
}
if _, err := f.Write([]byte(body)); err != nil {
t.Fatal(err)
}
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
return buf.Bytes()
}
const (
gatePackageRelNS = "http://schemas.openxmlformats.org/package/2006/relationships"
gateOfficeRel = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
gateStrictOffice = "http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument"
gateContentNS = "http://schemas.openxmlformats.org/package/2006/content-types"
gateCTXML = `<?xml version="1.0"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="xml" ContentType="application/xml"/><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/></Types>`
gateSlide = `<?xml version="1.0"?><p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:cSld><p:spTree/></p:cSld></p:sld>`
gateLayout = `<?xml version="1.0"?><p:sldLayout xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:cSld><p:spTree/></p:cSld></p:sldLayout>`
gateMaster = `<?xml version="1.0"?><p:sldMaster xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:cSld><p:spTree/></p:cSld><p:sldLayoutIdLst><p:sldLayoutId id="2147483647" r:id="rId1"/></p:sldLayoutIdLst></p:sldMaster>`
gateTheme = `<?xml version="1.0"?><a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"/>`
)
func relXML(rels ...string) string {
return `<?xml version="1.0"?><Relationships xmlns="` + gatePackageRelNS + `">` +
strings.Join(rels, "") + `</Relationships>`
}
func gateOfficePackage(t *testing.T, rels, contentTypes, mainPart string) []byte {
t.Helper()
files := map[string]string{
"_rels/.rels": rels,
"[Content_Types].xml": contentTypes,
}
if mainPart != "" {
files[strings.TrimPrefix(mainPart, "/")] = `<?xml version="1.0"?><root/>`
}
return zipOf(t, files)
}
func gateOfficeRelXML(relType, target, targetMode string) string {
mode := ""
if targetMode != "" {
mode = ` TargetMode="` + targetMode + `"`
}
return relXML(`<Relationship Id="rId1" Type="` + relType + `" Target="` + target + `"` + mode + `/>`)
}
func gateOverrideCT(part, contentType string) string {
return `<?xml version="1.0"?><Types xmlns="` + gateContentNS + `"><Override PartName="/` +
strings.TrimPrefix(part, "/") + `" ContentType="` + contentType + `"/></Types>`
}
const (
relSlide = `<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/>`
relMaster = `<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/>`
relLayout = `<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>`
relToMaster = `<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="../slideMasters/slideMaster1.xml"/>`
relTheme = `<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="../theme/theme1.xml"/>`
)
// gateCT 内容类型表:传入要 Override 的结构部件(缺了的用例自己构造残缺形态)。
func gateCT(parts ...string) string {
cts := map[string]string{
"ppt/presentation.xml": ctPresentation,
"ppt/slides/slide1.xml": ctSlide,
"ppt/slides/slide2.xml": ctSlide,
"ppt/slides/slide3.xml": ctSlide,
"ppt/slideLayouts/slideLayout1.xml": ctSlideLayout,
"ppt/slideMasters/slideMaster1.xml": ctSlideMaster,
"ppt/theme/theme1.xml": ctTheme,
}
out := `<?xml version="1.0"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="xml" ContentType="application/xml"/><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>`
for _, p := range parts {
out += `<Override PartName="/` + p + `" ContentType="` + cts[p] + `"/>`
}
return out + `</Types>`
}
var gateAllParts = []string{
"ppt/presentation.xml", "ppt/slides/slide1.xml",
"ppt/slideLayouts/slideLayout1.xml", "ppt/slideMasters/slideMaster1.xml", "ppt/theme/theme1.xml",
}
// gateValidPptx 是结构合法的最小演示稿:r:id 引用、ID 清单与关系表一致、
// 内容类型全量覆盖——与 python-pptx 产物的关键形态一致。
func gateValidPptx(t *testing.T) []byte {
t.Helper()
return zipOf(t, map[string]string{
"[Content_Types].xml": gateCT(gateAllParts...),
"_rels/.rels": relXML(),
"ppt/presentation.xml": `<?xml version="1.0"?><p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId2"/></p:sldMasterIdLst><p:sldIdLst><p:sldId id="256" r:id="rId1"/></p:sldIdLst></p:presentation>`,
"ppt/_rels/presentation.xml.rels": relXML(relSlide, relMaster),
"ppt/slides/slide1.xml": gateSlide,
"ppt/slides/_rels/slide1.xml.rels": relXML(relLayout),
"ppt/slideLayouts/slideLayout1.xml": gateLayout,
"ppt/slideLayouts/_rels/slideLayout1.xml.rels": relXML(relToMaster),
"ppt/slideMasters/slideMaster1.xml": gateMaster,
"ppt/slideMasters/_rels/slideMaster1.xml.rels": relXML(relLayout, relTheme),
"ppt/theme/theme1.xml": gateTheme,
})
}
// gateIncident2Pptx 复刻第二起事故(部件齐全但引用断裂,上版门禁放行的形态):
// presentation/母版根元素漏 xmlns:r;sldIdLst 声明 3 页但关系表只有 rId3→slide1;
// [Content_Types] 缺 slide2/3 的 Override。三个 fix 开关逐项修复以隔离断言。
func gateIncident2Pptx(t *testing.T, fixNS, fixRels, fixCT bool) []byte {
t.Helper()
nsDecl := ` xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"`
pns, mns := "", ""
if fixNS {
pns, mns = nsDecl, nsDecl
}
pres := `<?xml version="1.0"?><p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"` + pns + `><p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst><p:sldIdLst><p:sldId id="256" r:id="rId3"/><p:sldId id="257" r:id="rId4"/><p:sldId id="258" r:id="rId5"/></p:sldIdLst></p:presentation>`
master := `<?xml version="1.0"?><p:sldMaster xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"` + mns + `><p:cSld><p:spTree/></p:cSld><p:sldLayoutIdLst><p:sldLayoutId id="2147483647" r:id="rId1"/></p:sldLayoutIdLst></p:sldMaster>`
rels := `<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/><Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/></Relationships>`
if fixRels {
rels = relXML(
`<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/>`,
`<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/>`,
`<Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide2.xml"/>`,
`<Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide3.xml"/>`)
}
ctParts := []string{"ppt/presentation.xml", "ppt/slideMasters/slideMaster1.xml",
"ppt/slideLayouts/slideLayout1.xml", "ppt/theme/theme1.xml", "ppt/slides/slide1.xml"}
if fixCT {
ctParts = append(ctParts, "ppt/slides/slide2.xml", "ppt/slides/slide3.xml")
}
return zipOf(t, map[string]string{
"[Content_Types].xml": gateCT(ctParts...),
"_rels/.rels": relXML(),
"ppt/presentation.xml": pres,
"ppt/_rels/presentation.xml.rels": rels,
"ppt/slides/slide1.xml": gateSlide,
"ppt/slides/slide2.xml": gateSlide,
"ppt/slides/slide3.xml": gateSlide,
"ppt/slides/_rels/slide1.xml.rels": relXML(relLayout),
"ppt/slides/_rels/slide2.xml.rels": relXML(relLayout),
"ppt/slides/_rels/slide3.xml.rels": relXML(relLayout),
"ppt/slideLayouts/slideLayout1.xml": gateLayout,
"ppt/slideLayouts/_rels/slideLayout1.xml.rels": relXML(relToMaster),
"ppt/slideMasters/slideMaster1.xml": master,
"ppt/slideMasters/_rels/slideMaster1.xml.rels": relXML(relLayout, relTheme),
"ppt/theme/theme1.xml": gateTheme,
})
}
func TestOoxmlGateValidPptx(t *testing.T) {
if err := ooxmlStructuralCheck("交付物.pptx", gateValidPptx(t)); err != nil {
t.Fatalf("valid pptx must pass: %v", err)
}
}
func TestOoxmlGateIncident1PartsMissing(t *testing.T) {
// 第一类事故:部件整体缺失(无母版/主题/docProps、slide 无关系表)。
broken := zipOf(t, map[string]string{
"[Content_Types].xml": gateCTXML,
"_rels/.rels": relXML(),
"ppt/presentation.xml": `<?xml version="1.0"?><p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:sldIdLst><p:sldId id="2"/></p:sldIdLst></p:presentation>`,
"ppt/_rels/presentation.xml.rels": relXML(relSlide),
"ppt/slides/slide1.xml": gateSlide,
"ppt/slideLayouts/slideLayout1.xml": gateLayout,
})
err := ooxmlStructuralCheck("事故一.pptx", broken)
if err == nil {
t.Fatal("incident-shaped pptx must be rejected")
}
for _, want := range []string{"母版", "sldMasterIdLst"} {
if !strings.Contains(err.Error(), want) {
t.Fatalf("error %q must mention %q", err, want)
}
}
}
// 第二类事故:部件齐全但引用断裂——上一版门禁放行、Office 拒开的形态。
func TestOoxmlGateIncident2ReferenceBreaks(t *testing.T) {
cases := []struct {
name string
fixNS, fixRels, fixCT bool
want string
}{
{"undeclared-ns", false, true, true, "未声明的命名空间前缀 xmlns:r"},
{"sldid-dangling-rid", true, false, true, "sldIdLst 引用的 slide 在关系表中不存在"},
{"ct-missing-slides", true, true, false, "Content_Types"},
}
for _, tc := range cases {
err := ooxmlStructuralCheck("事故二.pptx", gateIncident2Pptx(t, tc.fixNS, tc.fixRels, tc.fixCT))
if err == nil {
t.Fatalf("%s: must be rejected", tc.name)
}
if !strings.Contains(err.Error(), tc.want) {
t.Fatalf("%s: error %q must mention %q", tc.name, err, tc.want)
}
}
// 三处缺陷全部修复后必须放行(对应 P4 修复产物的形态)。
if err := ooxmlStructuralCheck("修复后.pptx", gateIncident2Pptx(t, true, true, true)); err != nil {
t.Fatalf("fully repaired incident-2 package must pass: %v", err)
}
}
func TestOoxmlGatePptxChainBreaks(t *testing.T) {
base := func(t *testing.T) map[string]string {
t.Helper()
valid := gateValidPptx(t)
zr, err := zip.NewReader(bytes.NewReader(valid), int64(len(valid)))
if err != nil {
t.Fatal(err)
}
m := map[string]string{}
for _, f := range zr.File {
b, err := readZipFile(f)
if err != nil {
t.Fatal(err)
}
m[f.Name] = string(b)
}
return m
}
cases := []struct {
name string
mut func(m map[string]string)
want string
}{
{"slide-no-rels", func(m map[string]string) {
delete(m, "ppt/slides/_rels/slide1.xml.rels")
}, "关系表缺失"},
{"slide-no-layout-rel", func(m map[string]string) {
m["ppt/slides/_rels/slide1.xml.rels"] = relXML()
}, "没有指向 slideLayout"},
{"layout-missing", func(m map[string]string) {
delete(m, "ppt/slideLayouts/slideLayout1.xml")
}, "缺少必备部件"},
{"layout-no-master-rel", func(m map[string]string) {
m["ppt/slideLayouts/_rels/slideLayout1.xml.rels"] = relXML()
}, "没有指向 slideMaster"},
{"layout-master-not-listed", func(m map[string]string) {
m["ppt/slideLayouts/_rels/slideLayout1.xml.rels"] = relXML(`<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="../slideMasters/slideMaster2.xml"/>`)
m["ppt/slideMasters/slideMaster2.xml"] = gateMaster
m["ppt/slideMasters/_rels/slideMaster2.xml.rels"] = relXML(relLayout, relTheme)
}, "未在 presentation 的 sldMasterIdLst 中声明"},
{"master-no-theme-rel", func(m map[string]string) {
m["ppt/slideMasters/_rels/slideMaster1.xml.rels"] = relXML(relLayout)
}, "没有指向 theme"},
{"theme-missing", func(m map[string]string) {
delete(m, "ppt/theme/theme1.xml")
}, "缺少必备部件"},
{"master-no-layout-idlst", func(m map[string]string) {
m["ppt/slideMasters/slideMaster1.xml"] = strings.Replace(gateMaster, `<p:sldLayoutIdLst><p:sldLayoutId id="2147483647" r:id="rId1"/></p:sldLayoutIdLst>`, "", 1)
}, "缺少 <sldLayoutIdLst>"},
{"pres-no-master-idlst", func(m map[string]string) {
m["ppt/presentation.xml"] = strings.Replace(
m["ppt/presentation.xml"],
`<p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId2"/></p:sldMasterIdLst>`, "", 1)
}, "sldMasterIdLst"},
{"pres-slide-rel-dangling", func(m map[string]string) {
m["ppt/_rels/presentation.xml.rels"] = relXML(relMaster)
}, "sldIdLst 引用的 slide 在关系表中不存在"},
{"pres-extra-slide-rel", func(m map[string]string) {
m["ppt/_rels/presentation.xml.rels"] = relXML(relSlide, relMaster,
`<Relationship Id="rId9" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide2.xml"/>`)
m["ppt/slides/slide2.xml"] = gateSlide
m["ppt/slides/_rels/slide2.xml.rels"] = relXML(relLayout)
}, "数量不一致"},
{"xml-malformed", func(m map[string]string) {
m["ppt/slides/slide1.xml"] = `<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">`
}, "不是合法 XML"},
{"ct-slide-mismatch", func(m map[string]string) {
m["[Content_Types].xml"] = strings.Replace(gateCT(gateAllParts...), ctSlide, "application/xml", 1)
}, "内容类型不符"},
}
for _, tc := range cases {
m := base(t)
tc.mut(m)
err := ooxmlStructuralCheck("deck.pptx", zipOf(t, m))
if err == nil {
t.Fatalf("%s: must be rejected", tc.name)
}
if !strings.Contains(err.Error(), tc.want) {
t.Fatalf("%s: error %q must mention %q", tc.name, err, tc.want)
}
}
}
func readZipFile(f *zip.File) ([]byte, error) {
rc, err := f.Open()
if err != nil {
return nil, err
}
defer rc.Close()
var buf bytes.Buffer
if _, err := buf.ReadFrom(rc); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func TestOoxmlGateDocxXlsxAndPassthrough(t *testing.T) {
validDocx := zipOf(t, map[string]string{
"[Content_Types].xml": gateCTXML,
"word/document.xml": `<?xml version="1.0"?><w:document xmlns:w="urn:x"/>`,
})
if err := ooxmlStructuralCheck("方案.docx", validDocx); err != nil {
t.Fatalf("valid docx must pass: %v", err)
}
if err := ooxmlStructuralCheck("方案.docx", zipOf(t, map[string]string{"[Content_Types].xml": gateCTXML})); err == nil {
t.Fatal("docx without document.xml must be rejected")
}
// 命名空间严格化同样覆盖 docx:正文用了未声明前缀要拦。
badDocx := zipOf(t, map[string]string{
"[Content_Types].xml": gateCTXML,
"word/document.xml": `<?xml version="1.0"?><w:document><w:p r:id="r1"/></w:document>`,
})
err := ooxmlStructuralCheck("方案.docx", badDocx)
if err == nil || !strings.Contains(err.Error(), "未声明") {
t.Fatalf("docx with undeclared prefix must be rejected: %v", err)
}
validXlsx := zipOf(t, map[string]string{
"[Content_Types].xml": gateCTXML,
"xl/workbook.xml": `<?xml version="1.0"?><workbook xmlns="urn:x"/>`,
})
if err := ooxmlStructuralCheck("数据.xlsx", validXlsx); err != nil {
t.Fatalf("valid xlsx must pass: %v", err)
}
if err := ooxmlStructuralCheck("备注.md", []byte("# 标题\n正文")); err != nil {
t.Fatalf("non-office files must pass through: %v", err)
}
if err := ooxmlStructuralCheck("坏.pptx", []byte("这是一段文本,不是 zip")); err == nil {
t.Fatal("text masquerading as pptx must be rejected")
}
}
// 登记口集成:残缺包在 register_artifact 处被拒并给出重建指引;合法包照常登记。
func TestRegisterArtifactStructuralGate(t *testing.T) {
r := NewRegistry()
if err := RegisterExecTools(r); err != nil {
t.Fatal(err)
}
root := t.TempDir()
rc := &execFake{root: root, owner: "o1", conv: "c1", execOn: true}
sandbox := filepath.Join(root, "o1", "c1")
if err := os.MkdirAll(sandbox, 0o755); err != nil {
t.Fatal(err)
}
broken := filepath.Join(sandbox, "坏.pptx")
if err := os.WriteFile(broken, gateIncident2Pptx(t, false, false, false), 0o644); err != nil {
t.Fatal(err)
}
res := interpCall(t, r, rc, "register_artifact", `{"path":"坏.pptx"}`)
if res.Status != "error" || !strings.Contains(res.Output, "结构自检未通过") || !strings.Contains(res.Output, "housestyle") {
t.Fatalf("broken pptx must be refused with rebuild hint: %s %s", res.Status, res.Output)
}
good := filepath.Join(sandbox, "好.pptx")
if err := os.WriteFile(good, gateValidPptx(t), 0o644); err != nil {
t.Fatal(err)
}
res = interpCall(t, r, rc, "register_artifact", `{"path":"好.pptx"}`)
if res.Status != "ok" || !strings.Contains(res.Output, "已登记交付物") {
t.Fatalf("valid pptx must register: %s %s", res.Status, res.Output)
}
}
func TestRegisterArtifactRejectsRenamedWordprocessingMLPackages(t *testing.T) {
r := NewRegistry()
if err := RegisterExecTools(r); err != nil {
t.Fatal(err)
}
root := t.TempDir()
rc := &execFake{root: root, owner: "o1", conv: "c1", execOn: true}
sandbox := filepath.Join(root, "o1", "c1")
if err := os.MkdirAll(sandbox, 0o755); err != nil {
t.Fatal(err)
}
wordTypes := []struct {
name string
mainPart string
relType string
contentType string
}{
{"report.zip", "custom/main.xml", gateOfficeRel, "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"},
{"macro.bin", "word/document.xml", gateOfficeRel, "application/vnd.ms-word.document.macroEnabled.main+xml"},
{"template.data", "templates/body.xml", gateStrictOffice, "application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml"},
{"macro-template.payload", "package/document.xml", gateOfficeRel, "application/vnd.ms-word.template.macroEnabledTemplate.main+xml"},
}
for _, test := range wordTypes {
t.Run(test.name, func(t *testing.T) {
pkg := gateOfficePackage(t,
gateOfficeRelXML(test.relType, test.mainPart, ""),
gateOverrideCT(test.mainPart, test.contentType),
test.mainPart,
)
if err := os.WriteFile(filepath.Join(sandbox, test.name), pkg, 0o644); err != nil {
t.Fatal(err)
}
res := interpCall(t, r, rc, "register_artifact", `{"path":`+mustJSON(t, test.name)+`}`)
if res.Status != "error" || !strings.Contains(res.Output, "direct Word registration is forbidden") {
t.Fatalf("renamed WordprocessingML package must be refused: %s %s", res.Status, res.Output)
}
})
}
defaultTyped := gateOfficePackage(t,
gateOfficeRelXML(gateOfficeRel, "alternate/main.wml", ""),
`<?xml version="1.0"?><Types xmlns="`+gateContentNS+`"><Default Extension="wml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>`,
"alternate/main.wml",
)
if err := os.WriteFile(filepath.Join(sandbox, "default-typed.pkg"), defaultTyped, 0o644); err != nil {
t.Fatal(err)
}
res := interpCall(t, r, rc, "register_artifact", `{"path":"default-typed.pkg"}`)
if res.Status != "error" || !strings.Contains(res.Output, "direct Word registration is forbidden") {
t.Fatalf("default-typed WordprocessingML package must be refused: %s %s", res.Status, res.Output)
}
fakeWordPart := zipOf(t, map[string]string{
"word/document.xml": `<?xml version="1.0"?><not-word/>`,
"data/payload.json": `{"ok":true}`,
})
if err := os.WriteFile(filepath.Join(sandbox, "same-name.zip"), fakeWordPart, 0o644); err != nil {
t.Fatal(err)
}
res = interpCall(t, r, rc, "register_artifact", `{"path":"same-name.zip"}`)
if res.Status != "ok" || !strings.Contains(res.Output, "已登记交付物") {
t.Fatalf("ordinary ZIP with a same-name part must register: %s %s", res.Status, res.Output)
}
spreadsheet := gateOfficePackage(t,
gateOfficeRelXML(gateOfficeRel, "xl/workbook.xml", ""),
gateOverrideCT("xl/workbook.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"),
"xl/workbook.xml",
)
allowed := map[string][]byte{
"dataset.zip": zipOf(t, map[string]string{"data/payload.json": `{"ok":true}`}),
"spreadsheet.pkg": spreadsheet,
"payload.bin": {0x00, 0x01, 0x02, 0xff},
}
for name, content := range allowed {
if err := os.WriteFile(filepath.Join(sandbox, name), content, 0o644); err != nil {
t.Fatal(err)
}
res := interpCall(t, r, rc, "register_artifact", `{"path":`+mustJSON(t, name)+`}`)
if res.Status != "ok" || !strings.Contains(res.Output, "已登记交付物") {
t.Fatalf("non-Word artifact %s must register: %s %s", name, res.Status, res.Output)
}
}
}
func TestRegisterArtifactRejectsMalformedAmbiguousOrUnsafeOfficeDocumentMetadata(t *testing.T) {
r := NewRegistry()
if err := RegisterExecTools(r); err != nil {
t.Fatal(err)
}
root := t.TempDir()
rc := &execFake{root: root, owner: "o1", conv: "c1", execOn: true}
sandbox := filepath.Join(root, "o1", "c1")
if err := os.MkdirAll(sandbox, 0o755); err != nil {
t.Fatal(err)
}
const wordType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"
validCT := gateOverrideCT("custom/main.xml", wordType)
validRel := gateOfficeRelXML(gateOfficeRel, "custom/main.xml", "")
tests := []struct {
name string
rels string
contentTypes string
}{
{
name: "malformed-relationships.pkg",
rels: `<?xml version="1.0"?><Relationships xmlns="` + gatePackageRelNS + `"><Relationship Id="rId1" Type="` + gateOfficeRel + `" Target="custom/main.xml">`,
contentTypes: validCT,
},
{
name: "duplicate-office-relationships.pkg",
rels: relXML(
`<Relationship Id="rId1" Type="`+gateOfficeRel+`" Target="custom/main.xml"/>`,
`<Relationship Id="rId2" Type="`+gateOfficeRel+`" Target="other/main.xml"/>`,
),
contentTypes: validCT,
},
{
name: "malformed-content-types.pkg",
rels: validRel,
contentTypes: `<?xml version="1.0"?><Types xmlns="` + gateContentNS + `"><Override PartName="/custom/main.xml" ContentType="` + wordType + `">`,
},
{
name: "duplicate-content-type.pkg",
rels: validRel,
contentTypes: `<?xml version="1.0"?><Types xmlns="` + gateContentNS + `">` +
`<Override PartName="/custom/main.xml" ContentType="` + wordType + `"/>` +
`<Override PartName="/custom/main.xml" ContentType="application/xml"/></Types>`,
},
{
name: "missing-main-content-type.pkg",
rels: validRel,
contentTypes: `<?xml version="1.0"?><Types xmlns="` + gateContentNS + `"></Types>`,
},
{
name: "traversal-target.pkg",
rels: gateOfficeRelXML(gateOfficeRel, "../custom/main.xml", ""),
contentTypes: validCT,
},
{
name: "external-target.pkg",
rels: gateOfficeRelXML(gateOfficeRel, "https://example.invalid/main.xml", "External"),
contentTypes: validCT,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
pkg := gateOfficePackage(t, test.rels, test.contentTypes, "custom/main.xml")
if err := os.WriteFile(filepath.Join(sandbox, test.name), pkg, 0o644); err != nil {
t.Fatal(err)
}
res := interpCall(t, r, rc, "register_artifact", `{"path":`+mustJSON(t, test.name)+`}`)
if res.Status != "error" || !strings.Contains(res.Output, "direct Word registration is forbidden") {
t.Fatalf("unsafe officeDocument metadata must fail closed: %s %s", res.Status, res.Output)
}
})
}
}
func mustJSON(t *testing.T, value string) string {
t.Helper()
raw, err := json.Marshal(value)
if err != nil {
t.Fatal(err)
}
return string(raw)
}