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
This commit is contained in:
weichen
2026-10-15 22:46:21 +00:00
parent 6c01d00a3f
commit a2f4978be8
2 changed files with 479 additions and 27 deletions
+324 -10
View File
@@ -14,14 +14,23 @@ import (
"encoding/xml"
"fmt"
"io"
"net/url"
"path"
"path/filepath"
"strings"
)
const (
nsPackageRel = "http://schemas.openxmlformats.org/package/2006/relationships"
nsContentTypes = "http://schemas.openxmlformats.org/package/2006/content-types"
nsPresentationML = "http://schemas.openxmlformats.org/presentationml/2006/main"
nsOfficeDocRel = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
relOfficeDoc = nsOfficeDocRel + "/officeDocument"
relStrictOffice = "http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument"
ctWordDocument = "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"
ctWordTemplate = "application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml"
ctWordMacroDoc = "application/vnd.ms-word.document.macroEnabled.main+xml"
ctWordMacroTpl = "application/vnd.ms-word.template.macroEnabledTemplate.main+xml"
ctPresentation = "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"
ctSlide = "application/vnd.openxmlformats-officedocument.presentationml.slide+xml"
ctSlideLayout = "application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"
@@ -44,20 +53,324 @@ func ooxmlStructuralCheck(name string, content []byte) error {
}
}
// isWordprocessingMLPackage identifies Word OPC packages from their mandatory
// main document part instead of trusting the caller-controlled filename. The
// part is shared by document/template and macro-enabled variants.
// isWordprocessingMLPackage identifies Word OPC packages from the package-level
// officeDocument relationship and the resolved main part's content type. A
// purported officeDocument package with malformed, ambiguous, or unsafe
// metadata is treated as Word so the registration boundary fails closed.
func isWordprocessingMLPackage(content []byte) bool {
zr, err := zip.NewReader(bytes.NewReader(content), int64(len(content)))
if err != nil {
return false
}
for _, file := range zr.File {
if strings.EqualFold(strings.TrimPrefix(file.Name, "/"), "word/document.xml") {
return true
isWord, purportsOfficeDocument, err := ooxmlClassifyWordPackage(zr)
if err != nil {
return purportsOfficeDocument
}
return isWord
}
func ooxmlClassifyWordPackage(zr *zip.Reader) (isWord, purportsOfficeDocument bool, err error) {
relsFile, found, err := ooxmlUniquePart(zr, "_rels/.rels")
if err != nil {
return false, true, err
}
if !found {
return false, false, nil
}
rels, purportsOfficeDocument, err := ooxmlPackageRelationships(relsFile)
if err != nil {
return false, purportsOfficeDocument, err
}
var officeRels []ooxmlRel
for _, rel := range rels {
if rel.typ == relOfficeDoc || rel.typ == relStrictOffice {
officeRels = append(officeRels, rel)
}
}
return false
if len(officeRels) == 0 {
return false, false, nil
}
if len(officeRels) != 1 {
return false, true, fmt.Errorf("package relationships contain %d officeDocument relationships", len(officeRels))
}
mainPart, err := ooxmlSafePackageTarget(officeRels[0].target, officeRels[0].targetMode)
if err != nil {
return false, true, err
}
if _, found, err := ooxmlUniquePartFold(zr, mainPart); err != nil || !found {
if err != nil {
return false, true, err
}
return false, true, fmt.Errorf("officeDocument target %q is missing", mainPart)
}
contentTypesFile, found, err := ooxmlUniquePart(zr, "[Content_Types].xml")
if err != nil {
return false, true, err
}
if !found {
return false, true, fmt.Errorf("officeDocument package is missing [Content_Types].xml")
}
contentTypes, err := ooxmlParseContentTypes(contentTypesFile)
if err != nil {
return false, true, err
}
contentType, found := contentTypes.contentType(mainPart)
if !found {
return false, true, fmt.Errorf("officeDocument target %q has no content type", mainPart)
}
return ooxmlIsWordMainContentType(contentType), true, nil
}
type ooxmlContentTypeTable struct {
defaults map[string]string
overrides map[string]string
}
func (t ooxmlContentTypeTable) contentType(part string) (string, bool) {
if contentType, ok := t.overrides[strings.ToLower(part)]; ok {
return contentType, true
}
extension := strings.TrimPrefix(path.Ext(part), ".")
contentType, ok := t.defaults[strings.ToLower(extension)]
return contentType, ok
}
func ooxmlIsWordMainContentType(contentType string) bool {
switch contentType {
case ctWordDocument, ctWordTemplate, ctWordMacroDoc, ctWordMacroTpl:
return true
default:
return false
}
}
func ooxmlPackageRelationships(file *zip.File) ([]ooxmlRel, bool, error) {
rc, err := file.Open()
if err != nil {
return nil, false, fmt.Errorf("_rels/.rels cannot be read: %w", err)
}
defer rc.Close()
dec := xml.NewDecoder(rc)
var rels []ooxmlRel
seenIDs := map[string]bool{}
rootSeen := false
rootValid := false
depth := 0
purportsOfficeDocument := false
var metadataErr error
for {
tok, err := dec.Token()
if err == io.EOF {
break
}
if err != nil {
return nil, purportsOfficeDocument, fmt.Errorf("_rels/.rels is not valid XML: %w", err)
}
switch node := tok.(type) {
case xml.EndElement:
depth--
continue
case xml.StartElement:
depth++
if !rootSeen {
rootSeen = true
rootValid = node.Name.Space == nsPackageRel && node.Name.Local == "Relationships"
if !rootValid {
metadataErr = fmt.Errorf("_rels/.rels has an invalid Relationships root")
}
continue
}
var rel ooxmlRel
seenAttrs := map[string]bool{}
for _, attr := range node.Attr {
if attr.Name.Space != "" {
continue
}
if seenAttrs[attr.Name.Local] {
metadataErr = fmt.Errorf("_rels/.rels contains duplicate %s attributes", attr.Name.Local)
}
seenAttrs[attr.Name.Local] = true
switch attr.Name.Local {
case "Id":
rel.id = attr.Value
case "Type":
rel.typ = attr.Value
case "Target":
rel.target = attr.Value
case "TargetMode":
rel.targetMode = attr.Value
}
}
if rel.typ == relOfficeDoc || rel.typ == relStrictOffice {
purportsOfficeDocument = true
}
if depth != 2 || node.Name.Space != nsPackageRel || node.Name.Local != "Relationship" {
metadataErr = fmt.Errorf("_rels/.rels contains an invalid relationship element")
continue
}
if rel.id == "" || rel.typ == "" || rel.target == "" {
metadataErr = fmt.Errorf("_rels/.rels contains an incomplete Relationship")
continue
}
if seenIDs[rel.id] {
metadataErr = fmt.Errorf("_rels/.rels contains duplicate relationship Id %q", rel.id)
continue
}
seenIDs[rel.id] = true
rels = append(rels, rel)
}
}
if !rootSeen {
return nil, false, fmt.Errorf("_rels/.rels is empty")
}
if !rootValid || metadataErr != nil {
return nil, purportsOfficeDocument, metadataErr
}
return rels, purportsOfficeDocument, nil
}
func ooxmlParseContentTypes(file *zip.File) (ooxmlContentTypeTable, error) {
table := ooxmlContentTypeTable{
defaults: map[string]string{},
overrides: map[string]string{},
}
rc, err := file.Open()
if err != nil {
return table, fmt.Errorf("[Content_Types].xml cannot be read: %w", err)
}
defer rc.Close()
dec := xml.NewDecoder(rc)
rootSeen := false
for {
tok, err := dec.Token()
if err == io.EOF {
break
}
if err != nil {
return table, fmt.Errorf("[Content_Types].xml is not valid XML: %w", err)
}
se, ok := tok.(xml.StartElement)
if !ok {
continue
}
if !rootSeen {
rootSeen = true
if se.Name.Space != nsContentTypes || se.Name.Local != "Types" {
return table, fmt.Errorf("[Content_Types].xml has an invalid Types root")
}
continue
}
if se.Name.Space != nsContentTypes {
continue
}
attrs := map[string]string{}
for _, attr := range se.Attr {
if attr.Name.Space == "" {
attrs[attr.Name.Local] = attr.Value
}
}
switch se.Name.Local {
case "Default":
extension := strings.ToLower(attrs["Extension"])
contentType := attrs["ContentType"]
if extension == "" || strings.ContainsAny(extension, ".\\/ \t\r\n") || contentType == "" {
return table, fmt.Errorf("[Content_Types].xml contains an invalid Default")
}
if _, exists := table.defaults[extension]; exists {
return table, fmt.Errorf("[Content_Types].xml contains duplicate Default for %q", extension)
}
table.defaults[extension] = contentType
case "Override":
partName, err := ooxmlSafePartName(attrs["PartName"])
if err != nil || attrs["ContentType"] == "" {
return table, fmt.Errorf("[Content_Types].xml contains an invalid Override")
}
key := strings.ToLower(partName)
if _, exists := table.overrides[key]; exists {
return table, fmt.Errorf("[Content_Types].xml contains duplicate Override for %q", partName)
}
table.overrides[key] = attrs["ContentType"]
}
}
if !rootSeen {
return table, fmt.Errorf("[Content_Types].xml is empty")
}
return table, nil
}
func ooxmlSafePackageTarget(target, targetMode string) (string, error) {
if targetMode != "" && !strings.EqualFold(targetMode, "Internal") {
return "", fmt.Errorf("officeDocument target must be internal")
}
return ooxmlSafePartPath(target, strings.HasPrefix(target, "/"))
}
func ooxmlSafePartName(partName string) (string, error) {
if !strings.HasPrefix(partName, "/") {
return "", fmt.Errorf("OPC part name must be absolute")
}
return ooxmlSafePartPath(partName, true)
}
func ooxmlSafePartPath(raw string, absolute bool) (string, error) {
if raw == "" || strings.ContainsAny(raw, "\\\x00") {
return "", fmt.Errorf("unsafe OPC part path %q", raw)
}
parsed, err := url.Parse(raw)
if err != nil || parsed.IsAbs() || parsed.Host != "" || parsed.Opaque != "" || parsed.RawQuery != "" || parsed.Fragment != "" {
return "", fmt.Errorf("unsafe OPC part path %q", raw)
}
decoded, err := url.PathUnescape(parsed.EscapedPath())
if err != nil || strings.ContainsAny(decoded, "\\\x00") {
return "", fmt.Errorf("unsafe OPC part path %q", raw)
}
if absolute != strings.HasPrefix(decoded, "/") {
return "", fmt.Errorf("unsafe OPC part path %q", raw)
}
decoded = strings.TrimPrefix(decoded, "/")
if decoded == "" || strings.HasSuffix(decoded, "/") {
return "", fmt.Errorf("unsafe OPC part path %q", raw)
}
for _, segment := range strings.Split(decoded, "/") {
if segment == "" || segment == "." || segment == ".." {
return "", fmt.Errorf("unsafe OPC part path %q", raw)
}
}
return decoded, nil
}
func ooxmlUniquePart(zr *zip.Reader, name string) (*zip.File, bool, error) {
var found *zip.File
for _, file := range zr.File {
if file.Name != name {
continue
}
if found != nil {
return nil, false, fmt.Errorf("duplicate OPC metadata part %s", name)
}
found = file
}
return found, found != nil, nil
}
func ooxmlUniquePartFold(zr *zip.Reader, name string) (*zip.File, bool, error) {
var found *zip.File
for _, file := range zr.File {
if !strings.EqualFold(file.Name, name) {
continue
}
if found != nil {
return nil, false, fmt.Errorf("duplicate OPC part %s", name)
}
found = file
}
return found, found != nil, nil
}
// ooxmlCheckParts 校验容器与必备正文部件存在、XML 合法、命名空间前缀齐备
@@ -538,9 +851,10 @@ func ooxmlPart(zr *zip.Reader, name string) (*zip.File, error) {
// ooxmlRel 是 rels 部件里的一条关系:Id、类型 URI 与目标(原样保留相对形式)。
type ooxmlRel struct {
id string
typ string
target string
id string
typ string
target string
targetMode string
}
// ooxmlRels 解析 part 的关系表(<dir>/_rels/<base>.rels)。
+155 -17
View File
@@ -31,18 +31,47 @@ func zipOf(t *testing.T, files map[string]string) []byte {
}
const (
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"/>`
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="http://schemas.openxmlformats.org/package/2006/relationships">` +
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"/>`
@@ -365,20 +394,22 @@ func TestRegisterArtifactRejectsRenamedWordprocessingMLPackages(t *testing.T) {
wordTypes := []struct {
name string
mainPart string
relType string
contentType string
}{
{"report.zip", "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"},
{"macro.bin", "application/vnd.ms-word.document.macroEnabled.main+xml"},
{"template.data", "application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml"},
{"template.docx ", "application/vnd.ms-word.template.macroEnabledTemplate.main+xml"},
{"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) {
contentTypes := `<?xml version="1.0"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Override PartName="/word/document.xml" ContentType="` + test.contentType + `"/></Types>`
pkg := zipOf(t, map[string]string{
"[Content_Types].xml": contentTypes,
"word/document.xml": `<?xml version="1.0"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"/>`,
})
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)
}
@@ -389,9 +420,41 @@ func TestRegisterArtifactRejectsRenamedWordprocessingMLPackages(t *testing.T) {
})
}
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}`}),
"payload.bin": {0x00, 0x01, 0x02, 0xff},
"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 {
@@ -404,6 +467,81 @@ func TestRegisterArtifactRejectsRenamedWordprocessingMLPackages(t *testing.T) {
}
}
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)