Document Properties & Metadata
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
Baseline date: 2026-10-09. This page was written against Apache POI 5.5.1 (the latest release on that date; the 6.0 line
is in development and has not been released), Java 21 and later, Spring Boot 4.x on Spring Framework 7 and Jakarta EE 11.
Every Word and Excel file carries a small block of metadata next to its content: title, author, creation and
modification dates, the application that wrote it, a company name, keywords and any number of user-defined fields. File
explorers, search indexes, document management systems and the "Info" panel of Word and Excel read it, and so do
auditors. A server that generates reports therefore has two jobs: write correct metadata (and not leave the
default Apache POI creator and a "now" timestamp where something else is wanted), and treat the metadata of files it
receives as untrusted data. This page covers both, for the two physical formats described in
File Formats: OOXML & OLE2: the OOXML API
(POIXMLProperties, for .docx and .xlsx) and the OLE2 API (HPSF, for .xls and .doc). The examples use the
Bookshelf scenario described in the section landing page: books-inventory.xlsx,
loans-report.xlsx and loan-letter.docx exported with consistent metadata.
The components and jars are introduced in
Introduction & Architecture and the coordinates in
Setup & Dependencies. The HPSF classes live in the core poi
jar, POIXMLProperties in poi-ooxml, and HWPFDocument in poi-scratchpad.
Where the metadata lives
The same logical fields are stored in different places in the two families of formats:
| Format | Storage | POI entry point | Property groups |
|---|---|---|---|
OOXML ( |
Three small XML parts in the zip package: |
|
Core (Dublin Core style fields), extended (application statistics), custom (name and typed value) |
OLE2 ( |
Two property-set streams in the POIFS file system: |
|
Summary information, document summary information, custom properties (a dictionary inside the document summary stream) |
The \005 prefix is the control character U+0005 that Microsoft puts in front of these two stream names. Both formats
store the same kinds of values (strings, numbers, booleans, dates), but the field names differ (creator in OOXML,
author in HPSF) and so do the types available for custom properties. A program that handles both formats needs one
small adapter per format, as shown in the stamping section below.
A property is metadata, not content. It does not affect how the document renders, and neither POI nor Word or Excel validates it. Anyone can set any value, which matters when the file comes from a user.
OOXML properties with POIXMLProperties
POIXMLProperties (package org.apache.poi.ooxml) wraps the three property parts. It is created lazily by
getProperties() on an XSSFWorkbook, XWPFDocument or XMLSlideShow and gives three views:
| View | Part | Content |
|---|---|---|
|
|
The OPC core properties: |
|
|
Application-level information: |
|
|
User-defined name and value pairs. |
The core properties follow the Open Packaging Conventions (ECMA-376 part 2). Their element names come from Dublin Core
(dc:creator, dc:title, dcterms:created), which is why the Javadoc, the XML and the Word user interface use slightly
different labels for the same field. The extended and custom parts belong to the OOXML "document properties" part of
ECMA-376 and are specific to Office applications.
Writing core, extended and custom properties
The first example creates books-inventory.xlsx with all three groups, writes it and reads the properties back. It was
compiled and run against POI 5.5.1.
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Date;
import java.util.Optional;
import org.apache.poi.ooxml.POIXMLProperties;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.openxmlformats.schemas.officeDocument.x2006.extendedProperties.CTProperties;
public class XlsxProps {
public static void main(String[] args) throws IOException, InvalidFormatException {
Path file = Path.of("books-inventory.xlsx");
try (XSSFWorkbook workbook = new XSSFWorkbook()) {
workbook.createSheet("Books").createRow(0).createCell(0).setCellValue("Title");
POIXMLProperties props = workbook.getProperties();
// core properties (docProps/core.xml)
POIXMLProperties.CoreProperties core = props.getCoreProperties();
core.setTitle("Bookshelf inventory");
core.setSubjectProperty("Book catalogue");
core.setCreator("Bookshelf export service");
core.setKeywords("books, inventory");
core.setDescription("Catalogue exported from the Bookshelf application");
core.setCategory("Reports");
core.setLastModifiedByUser("bookshelf-batch");
core.setCreated(Optional.of(new Date()));
core.setModified(Optional.of(new Date()));
// extended properties (docProps/app.xml)
CTProperties app = props.getExtendedProperties().getUnderlyingProperties();
app.setApplication("Bookshelf Export 2.3");
app.setCompany("Bookshelf Library");
app.setManager("Catalogue team");
// custom properties (docProps/custom.xml)
POIXMLProperties.CustomProperties custom = props.getCustomProperties();
custom.addProperty("ExportedBy", "bookshelf-batch");
custom.addProperty("SchemaVersion", 3);
custom.addProperty("RowCount", 1250L);
custom.addProperty("ContainsPersonalData", false);
try (FileOutputStream out = new FileOutputStream(file.toFile())) {
workbook.write(out);
}
}
try (XSSFWorkbook workbook = new XSSFWorkbook(file.toFile())) {
POIXMLProperties props = workbook.getProperties();
POIXMLProperties.CoreProperties core = props.getCoreProperties();
System.out.println("title=" + core.getTitle());
System.out.println("creator=" + core.getCreator());
System.out.println("app=" + props.getExtendedProperties().getApplication());
POIXMLProperties.CustomProperties custom = props.getCustomProperties();
System.out.println("contains ExportedBy=" + custom.contains("ExportedBy"));
System.out.println("SchemaVersion=" + custom.getProperty("SchemaVersion").getI4());
}
}
}
Output:
title=Bookshelf inventory
creator=Bookshelf export service
app=Bookshelf Export 2.3
contains ExportedBy=true
SchemaVersion=3
The resulting parts are small and readable. docProps/core.xml is a single line of XML in which each setter produced one
element (dc:title, dc:creator, cp:keywords, cp:lastModifiedBy, dcterms:created and so on), docProps/app.xml holds
Application, Company and Manager, and docProps/custom.xml holds one property element per custom field, with a
pid (starting at 2), a fixed fmtid GUID, the name, and a typed child (vt:lpwstr, vt:i4, vt:r8 or vt:bool).
What a fresh document contains
A new XSSFWorkbook or XWPFDocument is not empty. Inspecting the package of a workbook created with no property calls
shows two parts written by POI: docProps/core.xml with dc:creator set to Apache POI and dcterms:created set to
the current UTC time, and docProps/app.xml with Application set to Apache POI. There is no title, no modified
date and no custom.xml. Files exported by a service therefore advertise "Apache POI" as their author unless the code
overrides it, which is rarely what an organisation wants to publish.
Pitfalls of the OOXML property API
-
Date setters take
Optional<Date>.setCreated(Optional<Date>)andsetModified(Optional<Date>)take an optional (aStringoverload also exists and throwsInvalidFormatExceptionfor a bad value), whilegetCreated()andgetModified()return a plainDate, null when absent. PassOptional.empty()to remove the field; the value is written in UTC as a W3CDTF string with second resolution. -
addPropertynever replaces. Adding a name twice throwsIllegalArgumentException("A property with this name already exists in the custom properties"). Checkcontains(name)first, as the stamping helper below does. There is noremoveProperty; to remove or change a custom property, edit the XMLBeans object returned bygetUnderlyingProperties(). -
The numeric overloads are
intanddouble.addProperty("RowCount", 1250L)compiles because thelongis widened todouble, and it is written asvt:r8(1250.0), not as an integer. Useintfor small counters. Excel and Word show the value as1250; a program reading it back must usegetR8(), notgetI8(). -
No automatic update on save. POI never sets
modified,lastModifiedByorrevisionwhen it writes a document. A file opened, edited and saved by POI keeps the old values, unlike a save from Word or Excel. Set them explicitly when the information matters. -
Property names are case sensitive and are shown as-is in the Word and Excel "Advanced properties" dialog.
-
commit()is called for you.write(OutputStream)serialises the three parts. CallPOIXMLProperties.commit()only if you write the underlyingOPCPackageyourself. -
Thumbnails.
POIXMLProperties.setThumbnail(String, InputStream)andgetThumbnailImage()read and write the package thumbnail (the preview shown by some file managers). It is a separate part and not a property.
Reading properties without building the document
Opening an XSSFWorkbook or XWPFDocument only to read the author or title parses every sheet or the whole body. The
OPC layer reads the core properties without that cost: OPCPackage.getPackageProperties() returns the
PackageProperties interface (getTitleProperty, getCreatorProperty, getCreatedProperty and so on, each an Optional). This
is the right tool for an upload endpoint or an indexer that only needs the metadata. Opening the package read-only also
avoids any write-back (see the pitfall on File constructors below). The second example, in the stamping section,
ends with this read.
OLE2 properties with HPSF
HPSF ("Horrible Property Set Format") is the POI component for Microsoft’s property-set serialisation, used by the binary Office formats and by every other OLE2 file. A property set is a typed list of numbered properties stored in one stream. Word, Excel and PowerPoint 97-2003 files carry two of them:
| Stream | POI class | Content |
|---|---|---|
|
|
Title, subject, author, keywords, comments, template, last author, revision number, total editing time, last printed, created, last saved, page, word and character counts, thumbnail, application name, security level. |
|
|
Category, presentation target, manager, company, byte, line, paragraph, slide, note, hidden-slide and multimedia-clip counts, document parts, heading pairs, language and a link to the custom property dictionary. |
Custom properties |
|
A name to value map stored in a second section of the document summary stream. Values can be strings, integers, longs, doubles, booleans and dates. |
HSSFWorkbook and HWPFDocument extend POIDocument, so both expose getSummaryInformation(),
getDocumentSummaryInformation() and, for new files, createInformationProperties(). Both getters return null when the
stream is missing, which is common for files produced by other libraries, so code must check for it.
For a new HSSFWorkbook the streams do not exist until createInformationProperties() is called; without that call the
file is written without them, and without an author, a title or a creation date.
Writing HPSF properties on an .xls file
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Date;
import org.apache.poi.hpsf.CustomProperties;
import org.apache.poi.hpsf.DocumentSummaryInformation;
import org.apache.poi.hpsf.SummaryInformation;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
public class XlsProps {
public static void main(String[] args) throws IOException {
Path file = Path.of("books-inventory.xls");
try (HSSFWorkbook workbook = new HSSFWorkbook()) {
workbook.createSheet("Books").createRow(0).createCell(0).setCellValue("Title");
// creates the \005SummaryInformation and \005DocumentSummaryInformation streams
workbook.createInformationProperties();
SummaryInformation summary = workbook.getSummaryInformation();
summary.setTitle("Bookshelf inventory");
summary.setSubject("Book catalogue");
summary.setAuthor("Bookshelf export service");
summary.setKeywords("books, inventory");
summary.setComments("Catalogue exported from the Bookshelf application");
summary.setApplicationName("Bookshelf Export 2.3");
summary.setCreateDateTime(new Date());
summary.setLastSaveDateTime(new Date());
DocumentSummaryInformation docSummary = workbook.getDocumentSummaryInformation();
docSummary.setCompany("Bookshelf Library");
docSummary.setManager("Catalogue team");
docSummary.setCategory("Reports");
CustomProperties custom = docSummary.getCustomProperties();
if (custom == null) {
custom = new CustomProperties();
}
custom.put("ExportedBy", "bookshelf-batch");
custom.put("SchemaVersion", 3);
custom.put("RowCount", 1250L);
custom.put("ContainsPersonalData", Boolean.FALSE);
docSummary.setCustomProperties(custom);
try (FileOutputStream out = new FileOutputStream(file.toFile())) {
workbook.write(out);
}
}
try (POIFSFileSystem fs = new POIFSFileSystem(file.toFile());
HSSFWorkbook workbook = new HSSFWorkbook(fs)) {
SummaryInformation summary = workbook.getSummaryInformation();
DocumentSummaryInformation docSummary = workbook.getDocumentSummaryInformation();
if (summary == null || docSummary == null) {
System.out.println("no property sets in this file");
return;
}
System.out.println("title=" + summary.getTitle());
System.out.println("author=" + summary.getAuthor());
System.out.println("company=" + docSummary.getCompany());
CustomProperties custom = docSummary.getCustomProperties();
System.out.println("SchemaVersion=" + custom.get("SchemaVersion"));
System.out.println("RowCount=" + custom.get("RowCount") + " " + custom.get("RowCount").getClass().getSimpleName());
}
}
}
Output (compiled and run against POI 5.5.1):
title=Bookshelf inventory
author=Bookshelf export service
company=Bookshelf Library
SchemaVersion=3
RowCount=1250 Long
The same calls work for a .doc: open it with new HWPFDocument(fs.getRoot()) (the poi-scratchpad jar) and call
getSummaryInformation(). HWPFDocument reads the property sets of an existing file; it cannot create a
new .doc from scratch, so the "create" half of this section applies to .xls only. See
Legacy .xls with HSSF and
Legacy .doc with HWPF for the rest of the two APIs.
HPSF types and custom properties
CustomProperties implements Map<String, Object>. The value type chosen on put decides the stored variant type, and
the type is preserved on a round trip: a String stays a String, an Integer an Integer, a Long a Long, a
Double a Double, a Boolean a Boolean and a java.util.Date a Date. This is a difference from OOXML, where long
becomes double. (When a program imports both org.apache.poi.hpsf.* and java.util.*, Date is ambiguous because HPSF
has its own Date type; qualify one of them.) Property names are unique, and put with an existing name replaces the value.
CustomProperties.setCodepage(int) controls the encoding of string values; the default is Unicode.
| Java value | OOXML custom property | HPSF custom property |
|---|---|---|
|
|
string (Unicode) |
|
|
32-bit integer |
|
|
64-bit integer |
|
|
double |
|
|
boolean |
|
no overload in |
date (stored as a FILETIME) |
Reading a property set without HSSF or HWPF
An upload pipeline sometimes needs the metadata of any OLE2 file without parsing the workbook. The POIFS file system
exposes the property-set streams directly, and PropertySetFactory.create(InputStream) returns the matching
SummaryInformation or DocumentSummaryInformation:
import java.io.File;
import java.io.IOException;
import org.apache.poi.hpsf.PropertySet;
import org.apache.poi.hpsf.PropertySetFactory;
import org.apache.poi.hpsf.SummaryInformation;
import org.apache.poi.poifs.filesystem.DirectoryNode;
import org.apache.poi.poifs.filesystem.DocumentInputStream;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
public class OleSummary {
public static void main(String[] args) throws Exception {
try (POIFSFileSystem fs = new POIFSFileSystem(new File("books-inventory.xls"))) {
DirectoryNode root = fs.getRoot();
if (root.hasEntry(SummaryInformation.DEFAULT_STREAM_NAME)) {
try (DocumentInputStream in = root.createDocumentInputStream(SummaryInformation.DEFAULT_STREAM_NAME)) {
PropertySet ps = PropertySetFactory.create(in);
if (ps instanceof SummaryInformation si) {
System.out.println("author=" + si.getAuthor() + " app=" + si.getApplicationName());
}
}
}
}
}
}
Output: author=Bookshelf export service app=Bookshelf Export 2.3.
Stamping export metadata
For the exports of the Bookshelf application, every generated file should say who produced it, with which software, when, and under which export identifier, so that a support engineer can correlate a file found in a mailbox with a log entry. Four fields are enough:
| Information | OOXML field | OLE2 (HPSF) field |
|---|---|---|
Author / creating principal (a service name, not a person) |
|
|
Generator and version |
|
|
Export timestamp |
|
|
Export identifier, schema version |
custom properties |
custom properties |
Title |
|
|
The helper below stamps any OOXML document. POIXMLDocument is the common base class of XSSFWorkbook, XWPFDocument
and XSLFSlideShow, so one method serves the whole section. It takes a java.time.Clock instead of calling
new Date(), which makes the output testable and, as shown in the next section, reproducible.
import java.time.Clock;
import java.util.Date;
import java.util.Optional;
import org.apache.poi.ooxml.POIXMLDocument;
import org.apache.poi.ooxml.POIXMLProperties;
public final class ExportMetadata {
private final Clock clock;
private final String generator;
private final String exportedBy;
public ExportMetadata(Clock clock, String generator, String exportedBy) {
this.clock = clock;
this.generator = generator;
this.exportedBy = exportedBy;
}
public void stamp(POIXMLDocument document, String title, String exportId) {
POIXMLProperties props = document.getProperties();
Date now = Date.from(clock.instant());
POIXMLProperties.CoreProperties core = props.getCoreProperties();
core.setTitle(title);
core.setCreator(exportedBy);
core.setLastModifiedByUser(exportedBy);
core.setCreated(Optional.of(now));
core.setModified(Optional.of(now));
props.getExtendedProperties().getUnderlyingProperties().setApplication(generator);
POIXMLProperties.CustomProperties custom = props.getCustomProperties();
if (!custom.contains("ExportId")) {
custom.addProperty("ExportId", exportId);
}
}
}
It is used for the three Bookshelf exports. The streaming workbook needs one extra step: SXSSFWorkbook is not a
POIXMLDocument, but getXSSFWorkbook() returns the underlying workbook whose properties are written when the SXSSF
workbook is written (see Streaming Writes with SXSSF).
The last lines read the metadata back through the cheap OPCPackage route:
import java.io.FileOutputStream;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackageAccess;
import org.apache.poi.openxml4j.opc.PackageProperties;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
public class Stamp {
public static void main(String[] args) throws Exception {
Clock clock = Clock.fixed(Instant.parse("2026-10-09T08:00:00Z"), ZoneOffset.UTC);
ExportMetadata metadata = new ExportMetadata(clock, "Bookshelf Export 2.3", "bookshelf-batch");
try (XSSFWorkbook workbook = new XSSFWorkbook();
FileOutputStream out = new FileOutputStream("books-inventory.xlsx")) {
workbook.createSheet("Books");
metadata.stamp(workbook, "Bookshelf inventory", "exp-0001");
workbook.write(out);
}
SXSSFWorkbook streaming = new SXSSFWorkbook(100);
try (streaming; FileOutputStream out = new FileOutputStream("loans-report.xlsx")) {
streaming.createSheet("Loans");
metadata.stamp(streaming.getXSSFWorkbook(), "Bookshelf loans", "exp-0002");
streaming.write(out);
} finally {
streaming.dispose();
}
try (XWPFDocument document = new XWPFDocument();
FileOutputStream out = new FileOutputStream("loan-letter.docx")) {
document.createParagraph().createRun().setText("Dear reader");
metadata.stamp(document, "Loan letter", "exp-0003");
document.write(out);
}
// cheap read without building the whole document model
try (OPCPackage pkg = OPCPackage.open("loan-letter.docx", PackageAccess.READ)) {
PackageProperties core = pkg.getPackageProperties();
System.out.println(core.getTitleProperty().orElse("-") + " | " + core.getCreatorProperty().orElse("-"));
}
}
}
Output (compiled and run against POI 5.5.1): Loan letter | bookshelf-batch. In the generated loans-report.xlsx,
app.xml contains Application set to Bookshelf Export 2.3 and custom.xml contains the ExportId property
exp-0002.
Guidance for the values themselves:
-
Use a service name as the author, not a user’s name, unless the user is the legitimate author of the document. An export is created by the system on behalf of a user, and a personal name in a file that the user emails elsewhere is a privacy leak. Put the requesting user’s id in a custom property only if it is needed for audit.
-
Do not put secrets, internal host names, SQL or file-system paths in properties. They are plain text in the zip and survive copy and paste of the file. This is the usual way internal details leak from generated reports.
-
Keep the generator string stable and versioned (
Bookshelf Export 2.3). It tells later readers which code produced a malformed file. -
Set the same stamps in every format. A user who downloads the same report as
.xlsxand as.xlsexpects the same author; use one adapter per format and a shared value object for the fields.
Reproducible output and SOURCE_DATE_EPOCH
A reproducible export produces byte-identical files for identical input, which allows content-based caching of generated
reports, deduplication in object storage, and tests that compare checksums. By default a POI export is not reproducible,
for two reasons: the core properties receive the current time, and the zip entries of the package are stamped with the
modification time of the moment they were written. Apache POI 5.4.0 (2025-01-08) added support for the
SOURCE_DATE_EPOCH convention of the Reproducible Builds project: the changelog entry reads "Add support for
SOURCE_DATE_EPOCH to allow to create reproducible binary files without creation/modification-timestamp being set".
What the support does, as observed with POI 5.5.1:
-
POI checks only whether the environment variable
SOURCE_DATE_EPOCHis defined. The value is not parsed; any value (a Unix timestamp such as the one the convention prescribes) switches the behaviour on. The check runs once, when the class is first loaded (Reproducibility.isSourceDateEpoch()), so the variable has to be set before the JVM starts. In code,Reproducibility.runWithSourceDateEpoch(IORunnable)also switches it on while the given block runs; its Javadoc says it is mostly used in tests, and it changes a static flag for the whole JVM. -
With it set, the zip entries in the OOXML package are written without a modification time (their time field is zero), so they no longer depend on when the export ran.
-
POI does not substitute the variable’s value into the document. The
dcterms:createdelement ofcore.xmlstill receives the current time, so the program must setcreated(andmodified) to a fixed value itself, for example the timestamp of the source data or of the request, or remove them withOptional.empty().
The third example demonstrates it. It writes the same workbook twice, two seconds apart, with fixed core properties, and
prints a short SHA-256 prefix of each result, then does the same for an .xls:
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.Date;
import java.util.HexFormat;
import java.util.Optional;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
public class Repro {
static byte[] xlsx(Instant fixed) throws IOException {
try (XSSFWorkbook wb = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
wb.createSheet("Books").createRow(0).createCell(0).setCellValue("Title");
var core = wb.getProperties().getCoreProperties();
core.setCreated(Optional.of(Date.from(fixed)));
core.setModified(Optional.of(Date.from(fixed)));
wb.write(out);
return out.toByteArray();
}
}
static byte[] xls() throws IOException {
try (HSSFWorkbook wb = new HSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
wb.createSheet("Books").createRow(0).createCell(0).setCellValue("Title");
wb.write(out);
return out.toByteArray();
}
}
static String sha(byte[] bytes) throws Exception {
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes)).substring(0, 12);
}
public static void main(String[] args) throws Exception {
Instant fixed = Instant.parse("2026-01-01T00:00:00Z");
String first = sha(xlsx(fixed));
Thread.sleep(2100);
String second = sha(xlsx(fixed));
System.out.println("xlsx: " + first + " " + second + " equal=" + first.equals(second));
String x1 = sha(xls());
Thread.sleep(2100);
String x2 = sha(xls());
System.out.println("xls: " + x1 + " " + x2 + " equal=" + x1.equals(x2));
}
}
java -cp "$CP" Repro
SOURCE_DATE_EPOCH=1767225600 java -cp "$CP" Repro
Measured output (the hash prefixes differ per machine, the equal values are the point):
| Run | .xlsx with fixed core dates |
.xls with no properties |
|---|---|---|
Without |
|
|
With |
|
|
The .xls was reproducible in this test in both runs because the HSSF file contained no timestamps: no
SummaryInformation stream was created. A file that sets createDateTime or lastSaveDateTime is only reproducible if
the program passes fixed values. In short, the environment variable removes the hidden wall-clock input from the zip layer;
removing the visible one (the core and summary dates) is the application’s job. To make the rest of the export
deterministic, also avoid anything else that depends on time or iteration order (HashMap ordering of rows, random
identifiers, LocalDate.now() in cell values), and do not rely on identical bytes across POI versions: a POI upgrade can
change the XML serialisation.
Source: https://poi.apache.org/changes.html#5.4.0 (the SOURCE_DATE_EPOCH convention itself is described at
https://reproducible-builds.org/docs/source-date-epoch/)
Metadata of uploaded files
The import path (see the section landing page) receives files from users. Their metadata is data from an untrusted source:
-
Treat every string as arbitrary text. An author field can hold markup, very long text, control characters or a formula prefix (equals, plus, minus or at sign). Escape it before it goes into HTML, logs or a CSV or spreadsheet export, and do not use it for authorisation ("the author is an admin").
-
Do not trust the dates. They are set by whichever program wrote the file and can be anything, including the future.
-
Do not trust
Application. It is useful to diagnose problems and to decide whether to apply workarounds for a producer, but anyone can claim to be Excel. -
Read only what you need and with the cheapest tool:
OPCPackage.getPackageProperties()for OOXML, the property-set streams for OLE2. The size limits and parsing hardening of the container apply to the metadata parts too (the security page of this section covers the limits). -
Metadata can leak the uploader’s identity. If the application stores or re-serves the file to other users, remove the creator, last editor, company and custom properties first.
The following program loads an uploaded workbook and writes a copy without the identifying fields. It also demonstrates a
trap of the File constructors:
import java.io.File;
import java.io.FileOutputStream;
import java.util.Optional;
import org.apache.poi.ooxml.POIXMLProperties;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackageAccess;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.openxmlformats.schemas.officeDocument.x2006.extendedProperties.CTProperties;
public class Scrub {
public static void main(String[] args) throws Exception {
try (OPCPackage pkg = OPCPackage.open(new File("books-inventory.xlsx"), PackageAccess.READ_WRITE);
XSSFWorkbook workbook = new XSSFWorkbook(pkg)) {
POIXMLProperties props = workbook.getProperties();
POIXMLProperties.CoreProperties core = props.getCoreProperties();
core.setCreator(null);
core.setLastModifiedByUser(null);
core.setModified(Optional.empty());
CTProperties app = props.getExtendedProperties().getUnderlyingProperties();
if (app.isSetCompany()) {
app.unsetCompany();
}
if (app.isSetManager()) {
app.unsetManager();
}
try (FileOutputStream out = new FileOutputStream("scrubbed.xlsx")) {
workbook.write(out);
}
pkg.revert(); // discard changes: closing a package opened from a File would save them over the original
}
}
}
Source: POIXMLProperties and
OPCPackage (open, revert) Javadoc.
The trap: a document opened with new XSSFWorkbook(File) or new XWPFDocument(OPCPackage) over a read-write package
saves its in-memory state back into that file when it is closed, even if the program only called write on another
stream. In a test with POI 5.5.1, changing the title on a workbook opened from File and writing it to a second file also
changed the title of the original file on close(). Opening with an InputStream (which copies the file into memory) or
calling OPCPackage.revert() before closing, as above, leaves the original untouched. A package opened with
PackageAccess.READ cannot be written at all (write throws InvalidOperationException), which makes it the
right choice for read-only inspection. This applies to the upload directory and to any template shared between requests,
which a modification of properties must never overwrite.
The unset methods of the XMLBeans type throw IndexOutOfBoundsException when the element is absent, hence the
isSet guards. The custom properties in the scrubbed copy are still present; remove them through the XMLBeans view
(getUnderlyingProperties()), or rebuild the document from the data. Other places that identify a user (comments and
tracked changes in .docx, the author of cell comments in .xlsx, document-level printer settings) are content, not
properties, and are not touched.
What POI does not provide
-
No synchronisation between formats: setting the author on an
XSSFWorkbookdoes nothing to an.xlsproduced from the same data, and no API converts aPOIXMLPropertiesinto HPSF property sets. -
No automatic statistics:
Pages,Words,Charactersand similar values inapp.xml, orPageCountandWordCountin HPSF, are not computed by POI. Word and Excel recompute them on save; POI writes whatever it finds. -
No change tracking or document history; the
revisionandversionfields are plain values. -
No validation of values, no limits on length, no check that a custom property name is legal in Office.
-
Custom property removal and a typed
Dateoverload for OOXML custom properties require the XMLBeans view (poi-ooxml-litemay not contain every class; usepoi-ooxml-fullif aCT*class is missing, as described in Setup & Dependencies).
Version notes
| Version | Change |
|---|---|
4.x and 5.x |
|
5.4.0 (2025-01-08) |
|
5.5.x |
The latest release on 2026-10-09 is 5.5.1. The examples on this page were compiled and run against it. |
6.0 (in development, not released on 2026-10-09) |
The announced changes (JDK 17 baseline, removal of deprecated APIs, hardening) may remove deprecated overloads; the metadata API surface above is expected to keep working, but check the 6.0 migration notes when it ships. |
Summary
-
OOXML metadata is three parts (
core.xml,app.xml,custom.xml) reached throughgetProperties(); OLE2 metadata is the\005SummaryInformationand\005DocumentSummaryInformationstreams, reached through HPSF. -
A new POI document carries
Apache POIas creator and application and the current creation time. Overwrite them in every export, with a service name, a versioned generator string, a fixed or request-derived timestamp and an export id. -
addPropertynever replaces andlongbecomesdoublein OOXML custom properties; HPSFCustomPropertiesis aMapthat preserves types. -
POI never updates
modified,lastModifiedByor statistics on save. -
SOURCE_DATE_EPOCH(POI 5.4.0) only removes zip entry timestamps; set the core dates yourself for byte-identical output, and define the variable before the JVM starts. -
Metadata of uploads is untrusted. Read it with
OPCPackageor the raw property-set streams, scrub identifying fields before redistributing, and never let aFile-opened package be closed read-write if the original must stay unchanged.
References
-
Apache POI home: https://poi.apache.org/
-
Release changes (5.4.0 entry for
SOURCE_DATE_EPOCH): https://poi.apache.org/changes.html -
HPSF component: https://poi.apache.org/components/hpsf/
-
HPSF how-to: https://poi.apache.org/components/hpsf/how-to.html
-
OpenXML4J: https://poi.apache.org/components/oxml4j/
-
Javadoc,
POIXMLProperties: https://poi.apache.org/apidocs/dev/org/apache/poi/ooxml/POIXMLProperties.html -
Javadoc,
POIXMLProperties.CoreProperties: https://poi.apache.org/apidocs/dev/org/apache/poi/ooxml/POIXMLProperties.CoreProperties.html -
Javadoc,
POIXMLProperties.CustomProperties: https://poi.apache.org/apidocs/dev/org/apache/poi/ooxml/POIXMLProperties.CustomProperties.html -
Javadoc,
PackageProperties: https://poi.apache.org/apidocs/dev/org/apache/poi/openxml4j/opc/PackageProperties.html -
Javadoc,
SummaryInformation: https://poi.apache.org/apidocs/dev/org/apache/poi/hpsf/SummaryInformation.html -
Javadoc,
DocumentSummaryInformation: https://poi.apache.org/apidocs/dev/org/apache/poi/hpsf/DocumentSummaryInformation.html -
Javadoc,
CustomProperties(HPSF): https://poi.apache.org/apidocs/dev/org/apache/poi/hpsf/CustomProperties.html -
Javadoc,
PropertySetFactory: https://poi.apache.org/apidocs/dev/org/apache/poi/hpsf/PropertySetFactory.html -
Javadoc,
POIDocument: https://poi.apache.org/apidocs/dev/org/apache/poi/POIDocument.html -
Official examples (
hpsf): https://github.com/apache/poi/tree/trunk/poi-examples/src/main/java/org/apache/poi/examples/hpsf -
Official examples (
xssf/usermodel): https://github.com/apache/poi/tree/trunk/poi-examples/src/main/java/org/apache/poi/examples/xssf/usermodel -
Reproducible Builds,
SOURCE_DATE_EPOCH: https://reproducible-builds.org/docs/source-date-epoch/ -
ECMA-376: https://ecma-international.org/publications-and-standards/standards/ecma-376/
-
Apache XMLBeans: https://xmlbeans.apache.org/