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:
+324
-10
@@ -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)。
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user