Skip to content

Instantly share code, notes, and snippets.

@mrled
Last active June 6, 2026 12:31
Show Gist options
  • Select an option

  • Save mrled/4c9e0ac820fd47d697138689f7474d90 to your computer and use it in GitHub Desktop.

Select an option

Save mrled/4c9e0ac820fd47d697138689f7474d90 to your computer and use it in GitHub Desktop.
exiv2 XML namespace corruption bug
*.jpg
*.xmp
poison-xmp
poison-xmp.dSYM

exiv2 XML namespace corruption bug

exiv2 will propagate whatever XML namespace it reads first into all files it writes going forward, such that reading metadata of a file with bad namespace values like xmlns:xmp="http://ns.abobe.com/xap/1.0/" (abobe, with 2 bs) or xmlns:xmpMM="(ttp://ns.adobe.com/xap/1.0/mm/" ((ttp instead of http) will cause all future metadata writes of any file to contain the same data.

To see how it works:

make demo

Which will show something like this:

make demo
./poison.sh
./poison-xmp 10946045463_8fd1864543_poisoned.jpg 10946045463_8fd1864543_o.jpg output.xmp
# ... snip ...

grep -oE 'xmlns:(xmp|xmpMM)="[^"]+"' output.xmp
xmlns:xmp="http://ns.abobe.com/xap/1.0/"
xmlns:xmpMM="(ttp://ns.adobe.com/xap/1.0/mm/"

In short, that will download a known-good JPEG, create a poisoned copy with junk data in XML namespaces, build a program that calls the exiv2 library function readMetadata() on the corrupted copy, calls readMetadata() on the original pristine copy, and writes out the metadata for the pristine copy to an XMP file. The result will have the same corrupted data as the poisoned file.

In detail

We'll use NASA's Hyperwall Shows the Sea Surface Temperature from the US Department of State's Flickr page.

./poison.sh in this directory downloads that file, then overwrites two characters from the xmp and xmpMM XML namespaces:

printf '%s' 'b' | dd of="$poison" bs=1 seek=19023 count=1 conv=notrunc 2>/dev/null
printf '%s' '(' | dd of="$poison" bs=1 seek=19209 count=1 conv=notrunc 2>/dev/null

poison-xmp.cpp in this directory will read metadata of first the poisoned and then the pristine file, then write the XMP for the pristine file.

The make demo target builds the C++ code and runs both of the above programs, then displays the xmp and xmpMM namespace of the output XMP file.

CXX ?= c++
EXIV2_PREFIX ?= $(shell brew --prefix exiv2 2>/dev/null || echo /usr/local)
PKG_CFLAGS := $(shell pkg-config --cflags exiv2 2>/dev/null)
PKG_LIBS := $(shell pkg-config --libs exiv2 2>/dev/null)
CXXFLAGS ?= -std=c++17 -O0 -g
ifeq ($(PKG_LIBS),)
CPPFLAGS += -I$(EXIV2_PREFIX)/include
LDLIBS += -L$(EXIV2_PREFIX)/lib -lexiv2
else
CPPFLAGS += $(PKG_CFLAGS)
LDLIBS += $(PKG_LIBS)
endif
poison-xmp: poison-xmp.cpp
$(CXX) $(CPPFLAGS) $(CXXFLAGS) $< $(LDLIBS) -o $@
clean:
rm -rf poison-xmp poison-xmp.dSYM *.xmp *.jpg
10946045463_8fd1864543_o.jpg 10946045463_8fd1864543_poisoned.jpg:
./poison.sh
output.xmp: poison-xmp 10946045463_8fd1864543_poisoned.jpg 10946045463_8fd1864543_o.jpg
./poison-xmp 10946045463_8fd1864543_poisoned.jpg 10946045463_8fd1864543_o.jpg output.xmp
.PHONY: demo
demo: output.xmp
@echo ""
cat output.xmp
@echo ""
grep -oE 'xmlns:(xmp|xmpMM)="[^"]+"' output.xmp
.PHONY: clean
#include <exiv2/exiv2.hpp>
#include <fstream>
#include <iostream>
int main(int argc, char **argv)
{
if (argc != 4)
{
std::cerr << "usage: " << argv[0] << " poisoned.jpg victim.jpg output.xmp\n";
return 2;
}
Exiv2::XmpParser::initialize();
auto poisoned = Exiv2::ImageFactory::open(argv[1]);
poisoned->readMetadata();
auto victim = Exiv2::ImageFactory::open(argv[2]);
victim->readMetadata();
Exiv2::XmpData xmp = victim->xmpData();
// Update something in the victim's "xmp" namespace:
xmp["Xmp.xmp.Rating"] = "1";
// Update something in the victim's "xmpMM" namespace:
xmp["Xmp.xmpMM.DocumentID"] = "xmp.did:exiv2-namespace-poison-repro";
// Encode the XMP data into a packet
// Omit the packet wrapper so the output is just the XMP content
std::string packet;
if (Exiv2::XmpParser::encode(packet, xmp, Exiv2::XmpParser::omitPacketWrapper))
{
std::cerr << "failed to encode XMP\n";
return 1;
}
// Save the XMP data to the output file
// Add the XML declaration to make it a valid XMP file
std::ofstream file(argv[3]);
file << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
<< packet << "\n";
Exiv2::XmpParser::terminate();
return 0;
}
#!/bin/sh
set -eu
url='https://www.flickr.com/photo_download.gne?id=10946045463&secret=8fd1864543&size=o&source=photoPageEngagement'
original='10946045463_8fd1864543_o.jpg'
poison='10946045463_8fd1864543_poisoned.jpg'
if ! test -f "$original"; then
curl -L --fail --silent --show-error --output "$original" "$url"
fi
cp -RL "$original" "$poison"
chmod 600 "$poison"
printf '%s' 'b' | dd of="$poison" bs=1 seek=19023 count=1 conv=notrunc 2>/dev/null
printf '%s' '(' | dd of="$poison" bs=1 seek=19209 count=1 conv=notrunc 2>/dev/null
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment