online / endpoints 59 / categories 14 / rate 60/min/ip /

GET /ooxml

GET /ooxml

Office Open XML container responses (DOCX, XLSX, PPTX) where one structural part of the package deliberately lies — wrong content type declaration, missing referenced part, dangling image relationship, or [Content_Types].xml claiming the package is multiple formats simultaneously. Macro-enabled containers, encrypted OOXML, and embedded OLE objects are deliberately not served — see the note below.

format docx (default; word/document.xml main part), xlsx (xl/workbook.xml + sheet1), pptx (full slide-master/slide-layout/theme triangle). Body content is identical hello-world text; only the container wrapping changes.
mode wrong-content-type (default; [Content_Types].xml advertises the main part as another OOXML format's content type), missing-part (root .rels references a part path that isn't in the ZIP; the part is at a typo'd path), dangling-relationship (main part's .rels declares an image relationship to media/image1.png that isn't in the package), format-confusion ([Content_Types].xml declares main parts for all three OOXML formats; only the current format's tree is present).

control Compare against the well-formed counterpart: not.catastrophic.io/ooxml side-by-side

build a request:

expect: An OOXML-shaped body (DOCX/XLSX/PPTX, picked by `format`) whose internal package structure contains one deliberate lie. The specific lie depends on `mode`. X-Chaos-Ooxml-Note explains the flaw in plain text. Cache-Control: no-transform prevents the CDN edge from rewriting the body.

bash
# Save the archive, then inspect the OOXML package structure.
curl -sD - 'https://chaos.catastrophic.io/ooxml?format=docx&mode=wrong-content-type' -o /tmp/chaos.docx | grep -i 'content-type\|x-chaos-ooxml'
unzip -p /tmp/chaos.docx '[Content_Types].xml' | head -20
unzip -l /tmp/chaos.docx
# python-docx / openpyxl / python-pptx are strict; chaos modes hard-fail.
import urllib.request, io, zipfile
resp = urllib.request.urlopen("https://chaos.catastrophic.io/ooxml?format=docx&mode=wrong-content-type")
raw = resp.read()
print("Content-Type:", resp.headers.get("Content-Type"))
print("X-Chaos-Ooxml-Mode:", resp.headers.get("X-Chaos-Ooxml-Mode"))
print("X-Chaos-Ooxml-Note:", resp.headers.get("X-Chaos-Ooxml-Note"))
zf = zipfile.ZipFile(io.BytesIO(raw))
print("Parts:", zf.namelist())
print("Content_Types.xml:")
print(zf.read("[Content_Types].xml").decode())
// Inspect headers; the body is binary, leave parsing to a library.
const res = await fetch("https://chaos.catastrophic.io/ooxml?format=docx&mode=wrong-content-type");
const bytes = new Uint8Array(await res.arrayBuffer());
console.log("Content-Type:", res.headers.get("content-type"));
console.log("X-Chaos-Ooxml-Format:", res.headers.get("x-chaos-ooxml-format"));
console.log("X-Chaos-Ooxml-Mode:", res.headers.get("x-chaos-ooxml-mode"));
console.log("X-Chaos-Ooxml-Note:", res.headers.get("x-chaos-ooxml-note"));
console.log("Bytes:", bytes.length);
package main

import (
    "archive/zip"
    "bytes"
    "fmt"
    "io"
    "net/http"
)

func main() {
    resp, _ := http.Get("https://chaos.catastrophic.io/ooxml?format=docx&mode=wrong-content-type")
    defer resp.Body.Close()
    body, _ := io.ReadAll(resp.Body)
    fmt.Println("Content-Type:", resp.Header.Get("Content-Type"))
    fmt.Println("X-Chaos-Ooxml-Note:", resp.Header.Get("X-Chaos-Ooxml-Note"))
    r, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
    if err != nil {
        fmt.Println("zip.NewReader error:", err)
        return
    }
    for _, f := range r.File {
        fmt.Printf("  %s\n", f.Name)
    }
}
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking"] }
//              zip = "2"
use std::io::Cursor;
fn main() -> Result<(), Box> {
    let resp = reqwest::blocking::get("https://chaos.catastrophic.io/ooxml?format=docx&mode=wrong-content-type")?;
    println!("Content-Type: {:?}", resp.headers().get("content-type"));
    println!("X-Chaos-Ooxml-Note: {:?}", resp.headers().get("x-chaos-ooxml-note"));
    let bytes = resp.bytes()?;
    let mut zf = zip::ZipArchive::new(Cursor::new(bytes.to_vec()))?;
    for i in 0..zf.len() {
        println!("  {}", zf.by_index(i)?.name());
    }
    Ok(())
}
// Java 11+ HttpClient.
import java.net.URI;
import java.net.http.*;
import java.util.zip.*;
import java.io.*;

public class OoxmlChaos {
    public static void main(String[] args) throws Exception {
        var client = HttpClient.newHttpClient();
        var req = HttpRequest.newBuilder(URI.create("https://chaos.catastrophic.io/ooxml?format=docx&mode=wrong-content-type")).build();
        var resp = client.send(req, HttpResponse.BodyHandlers.ofByteArray());
        System.out.println("Content-Type: " +
            resp.headers().firstValue("Content-Type").orElse(""));
        System.out.println("X-Chaos-Ooxml-Note: " +
            resp.headers().firstValue("X-Chaos-Ooxml-Note").orElse(""));
        try (var zis = new ZipInputStream(new ByteArrayInputStream(resp.body()))) {
            ZipEntry e;
            while ((e = zis.getNextEntry()) != null) {
                System.out.println("  " + e.getName());
            }
        }
    }
}
using System.IO.Compression;
using var client = new HttpClient();
var resp = await client.GetAsync("https://chaos.catastrophic.io/ooxml?format=docx&mode=wrong-content-type");
Console.WriteLine($"Content-Type: {resp.Content.Headers.ContentType}");
Console.WriteLine($"X-Chaos-Ooxml-Note: {resp.Headers.GetValues("X-Chaos-Ooxml-Note").FirstOrDefault()}");
var bytes = await resp.Content.ReadAsByteArrayAsync();
using var zf = new ZipArchive(new MemoryStream(bytes), ZipArchiveMode.Read);
foreach (var e in zf.Entries) {
    Console.WriteLine($"  {e.FullName}");
}
require "net/http"
require "tempfile"
uri = URI("https://chaos.catastrophic.io/ooxml?format=docx&mode=wrong-content-type")
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
    res = http.get(uri.request_uri)
    puts "Content-Type: #{res['Content-Type']}"
    puts "X-Chaos-Ooxml-Note: #{res['X-Chaos-Ooxml-Note']}"
    Tempfile.create(["chaos", ".docx"]) do |f|
        f.binmode; f.write(res.body); f.flush
        puts `unzip -l #{f.path}`
    end
end
# Save the OOXML package, then inspect [Content_Types].xml.
$tmp = Join-Path $env:TEMP 'chaos.docx'
$r = Invoke-WebRequest -Uri 'https://chaos.catastrophic.io/ooxml?format=docx&mode=wrong-content-type' -OutFile $tmp -PassThru
"Content-Type: $($r.Headers['Content-Type'])"
"X-Chaos-Ooxml-Mode: $($r.Headers['X-Chaos-Ooxml-Mode'])"
"X-Chaos-Ooxml-Note: $($r.Headers['X-Chaos-Ooxml-Note'])"
Add-Type -AssemblyName System.IO.Compression.FileSystem
$zip = [System.IO.Compression.ZipFile]::OpenRead($tmp)
$zip.Entries | Select-Object FullName, Length | Format-Table
$zip.Dispose()

What’s deliberately not served

Three OOXML attack classes are missing from the mode list on purpose:

  • Macro-enabled containers (DOCM, XLSM, PPTM with VBA payloads) — a working VBA macro is a known malware vector and would be effectively indistinguishable from a real attack.
  • Encrypted OOXML — wrapped in a Compound File Binary container rather than a ZIP, a separate format problem off the OOXML-chaos topic.
  • Embedded OLE objects — can carry executable payloads.

The four modes above keep the package-structure-disagreement story without serving anything that’s already an exploit shape, consistent with /zip.

Behaviour across readers

OOXML readers vary substantially in how strictly they validate the package. The same chaos mode can hard-fail in one reader and open cleanly in another — which is itself useful chaos. Observed from the canonical Python libraries:

Modepython-docxopenpyxlpython-pptx
wrong-content-typeValueError (not a Word file)OSError (no valid workbook part)ValueError (not a PowerPoint file)
missing-partKeyError (no word/document.xml)KeyError (no xl/workbook.xml)KeyError (no relationship of type officeDocument)
dangling-relationshipKeyError (no word/media/image1.png)KeyError (rId1)OPENED (relationship resolved lazily)
format-confusionOPENEDOPENEDOPENED

format-confusion opens cleanly in all three because the readers match the first matching Override entry and ignore the extras. Tools that enumerate every Override and dispatch by content type behave differently.