PhpWord fails to escape & in generated <w:t> content
Author: paulw3nerdCreated Jul 3, 2026Updated Jul 3, 2026
LabelsBug Report
### Describe the bug and add attachments
This is a PhpWord bug: Html::addHtml() correctly decodes & → & from HTML, but when PhpWord writes the OOXML, it fails to re-escape & back to & inside tags, so a broken file is created that Word can't open.
I fixed with the below:
//fix for phpword bug - PhpWord doesn't escape & in content, so post-fix the generated XML
$zipFix = new ZipArchive();
if ($zipFix->open($tempFile) === true) {
$docXml = $zipFix->getFromName('word/document.xml');
// Fix bare & that PhpWord failed to escape (only unescaped ones, not & etc.)
$docXml = preg_replace('/&(?!amp;|lt;|gt;|quot;|apos;|#\d+;|#x[0-9a-fA-F]+;)/i', '&', $docXml);
$zipFix->addFromString('word/document.xml', $docXml);
$zipFix->close();
}
### Expected behavior
Generate a file that can be opened by MS Word.
### Steps to reproduce
$phpWord = new PhpWord();
$phpWord->setDefaultFontName('Times New Roman');
$phpWord->setDefaultFontSize(10);
$margins = [
'marginLeft' => 720,
'marginRight' => 720,
'marginTop' => 720,
'marginBottom' => 720,
];
$section = $phpWord->addSection($margins);
$footer = $section->addFooter();
$footer->addText('TEST', array('italic' => true));
$htmlTestStr = '
';
$htmlTestStr .= '
';
$htmlTestStr .= '
';
$htmlTestStr = str_ireplace('
', '
', $htmlTestStr); // Escape bare ampersands that aren't already part of an HTML entity $htmlTestStr = preg_replace('/&(?!amp;|lt;|gt;|quot;|apos;|#\d+;|#x[0-9a-fA-F]+;)/i', '&', $htmlTestStr); //element,html,fullHTML,preserveWhiteSpace Html::addHtml($section, $htmlTestStr, true, false); $tempFile = tempnam(sys_get_temp_dir(), 'phpword'); $objWriter = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'Word2007'); $objWriter->save($tempFile); //fix for phpword bug - PhpWord doesn't escape & in content, so post-fix the generated XML $zipFix = new ZipArchive(); if ($zipFix->open($tempFile) === true) { $docXml = $zipFix->getFromName('word/document.xml'); // Fix bare & that PhpWord failed to escape (only unescaped ones, not & etc.) $docXml = preg_replace('/&(?!amp;|lt;|gt;|quot;|apos;|#\d+;|#x[0-9a-fA-F]+;)/i', '&', $docXml); $zipFix->addFromString('word/document.xml', $docXml); $zipFix->close(); } //Set download headers header('Content-Description: File Transfer'); header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename="test.docx"'); header('Content-Transfer-Encoding: binary'); header('Expires: 0'); header('Cache-Control: must-revalidate'); header('Pragma: public'); header('Content-Length: ' . filesize($tempFile)); // Output file and delete the temp file readfile($tempFile); unlink($tempFile); exit; ### PHPWord version(s) where the bug happened 1.4 ### PHP version(s) where the bug happened 8.3 ### Priority - [ ] I want to crowdfund the bug fix (with [@algora-io](https://docs.algora.io/bounties/overview)) and fund a community developer. - [ ] I want to pay the bug fix and fund a maintainer for that. (Contact @Progi1984)
Prepared By / Record & Return To:
', '
', $htmlTestStr); // Escape bare ampersands that aren't already part of an HTML entity $htmlTestStr = preg_replace('/&(?!amp;|lt;|gt;|quot;|apos;|#\d+;|#x[0-9a-fA-F]+;)/i', '&', $htmlTestStr); //element,html,fullHTML,preserveWhiteSpace Html::addHtml($section, $htmlTestStr, true, false); $tempFile = tempnam(sys_get_temp_dir(), 'phpword'); $objWriter = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'Word2007'); $objWriter->save($tempFile); //fix for phpword bug - PhpWord doesn't escape & in content, so post-fix the generated XML $zipFix = new ZipArchive(); if ($zipFix->open($tempFile) === true) { $docXml = $zipFix->getFromName('word/document.xml'); // Fix bare & that PhpWord failed to escape (only unescaped ones, not & etc.) $docXml = preg_replace('/&(?!amp;|lt;|gt;|quot;|apos;|#\d+;|#x[0-9a-fA-F]+;)/i', '&', $docXml); $zipFix->addFromString('word/document.xml', $docXml); $zipFix->close(); } //Set download headers header('Content-Description: File Transfer'); header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename="test.docx"'); header('Content-Transfer-Encoding: binary'); header('Expires: 0'); header('Cache-Control: must-revalidate'); header('Pragma: public'); header('Content-Length: ' . filesize($tempFile)); // Output file and delete the temp file readfile($tempFile); unlink($tempFile); exit; ### PHPWord version(s) where the bug happened 1.4 ### PHP version(s) where the bug happened 8.3 ### Priority - [ ] I want to crowdfund the bug fix (with [@algora-io](https://docs.algora.io/bounties/overview)) and fund a community developer. - [ ] I want to pay the bug fix and fund a maintainer for that. (Contact @Progi1984)
Source: PHPOffice/PHPWord