XLSX writer: XML escaping missing in font names and hyperlink targets (breaks openpyxl/lxml)
Summary
The OOXML XLSX writer in xlsxooxml.pas does not XML-escape two categories of strings when writing them as XML attribute values:
- Font names in
xl/styles.xml — quotes in font names are not escaped, producing invalid XML
- Hyperlink targets in
*.rels files — ampersands in URLs are not escaped, producing invalid XML
Both issues cause lxml.etree.XMLSyntaxError: attributes construct error when the generated .xlsx file is loaded by openpyxl (Python) or any other strict XML parser.
Environment
- fpspreadsheet version: as shipped with CodeTyphon (based on lazarus-ccr SVN, last synced 2026-03-15)
- File:
source/common/xlsxooxml.pas
- OS: Linux x86_64
- Compiler: Free Pascal 3.x (CodeTyphon distribution)
Bug #1: Unescaped quotes in font names
Location
xlsxooxml.pas, procedure TsSpreadOOXMLWriter.WriteFont, line ~5783:
if AFont.FontName <> '' then
s := s + Format('<%s val="%s" />', [NAME_TAG[UseInStyleNode], AFont.FontName]);
Problem
AFont.FontName is inserted directly into an XML attribute value without escaping. If the font name contains a double quote character ("), the resulting XML is malformed:
<!-- Produced (invalid): -->
<font><name val=""Google Sans"" /></font>
<!-- Expected (valid): -->
<font><name val=""Google Sans"" /></font>
Fix
Use the existing UTF8TextToXMLText() function (from fpsxmlcommon, already in the uses clause):
if AFont.FontName <> '' then
s := s + Format('<%s val="%s" />', [NAME_TAG[UseInStyleNode], UTF8TextToXMLText(AFont.FontName)]);
Bug #2: Unescaped ampersands in hyperlink targets
Location
xlsxooxml.pas, procedure TsSpreadOOXMLWriter.WriteWorksheetRels, line ~7372:
s := Format('Id="rId%d" Target="%s" TargetMode="External" Type="%s"',
[rId_Hyperlink, target, SCHEMAS_HYPERLINK]);
Problem
The target string (a URL from a hyperlink) is inserted directly into the XML attribute without escaping ampersands. URLs with query parameters (e.g., ?param=1¶m=2) produce invalid XML:
<!-- Produced (invalid): -->
<Relationship Id="rId1" Target="https://example.com/page?a=1&b=2" TargetMode="External" Type="..." />
<!-- Expected (valid): -->
<Relationship Id="rId1" Target="https://example.com/page?a=1&b=2" TargetMode="External" Type="..." />
Fix
s := Format('Id="rId%d" Target="%s" TargetMode="External" Type="%s"',
[rId_Hyperlink, UTF8TextToXMLText(target), SCHEMAS_HYPERLINK]);
Existing utility function
Both fixes use UTF8TextToXMLText() from the fpsxmlcommon unit, which is already in the uses clause of xlsxooxml.pas. This function correctly escapes & → &, < → <, > → >, " → ", ' → ', and avoids double-escaping existing entities.
Reproduction
- Create a workbook with a font whose name contains a double quote (e.g.,
"Google Sans")
- Add a hyperlink with a URL containing
& (e.g., https://example.com/page?a=1&b=2)
- Save as
.xlsx
- Attempt to load with
openpyxl.load_workbook("file.xlsx")
Result: lxml.etree.XMLSyntaxError: attributes construct error
Workaround (Python)
import zipfile, shutil, os, re
def fix_spready_xlsx(path):
tmp = path + ".tmp"
shutil.copy2(path, tmp)
with zipfile.ZipFile(tmp, 'r') as zin:
with zipfile.ZipFile(path, 'w', zipfile.ZIP_DEFLATED) as zout:
for item in zin.namelist():
data = zin.read(item)
if 'styles' in item or '.rels' in item:
text = data.decode('utf-8', errors='replace')
text = text.replace('""', '"')
text = re.sub(r'&(?!amp;|lt;|gt;|quot;|apos;|#\d+;|#x[0-9a-fA-F]+;)', '&', text)
data = text.encode('utf-8')
zout.writestr(item, data)
os.remove(tmp)
Impact
High — any application that combines fpspreadsheet-generated .xlsx files with strict XML parsers (openpyxl, lxml, .NET Open XML SDK) will fail to load them. This affects data pipelines, automated processing, and interoperability with the Python data ecosystem.
XLSX writer: XML escaping missing in font names and hyperlink targets (breaks openpyxl/lxml)
Summary
The OOXML XLSX writer in
xlsxooxml.pasdoes not XML-escape two categories of strings when writing them as XML attribute values:xl/styles.xml— quotes in font names are not escaped, producing invalid XML*.relsfiles — ampersands in URLs are not escaped, producing invalid XMLBoth issues cause
lxml.etree.XMLSyntaxError: attributes construct errorwhen the generated.xlsxfile is loaded byopenpyxl(Python) or any other strict XML parser.Environment
source/common/xlsxooxml.pasBug #1: Unescaped quotes in font names
Location
xlsxooxml.pas, procedureTsSpreadOOXMLWriter.WriteFont, line ~5783:Problem
AFont.FontNameis inserted directly into an XML attribute value without escaping. If the font name contains a double quote character ("), the resulting XML is malformed:Fix
Use the existing
UTF8TextToXMLText()function (fromfpsxmlcommon, already in theusesclause):Bug #2: Unescaped ampersands in hyperlink targets
Location
xlsxooxml.pas, procedureTsSpreadOOXMLWriter.WriteWorksheetRels, line ~7372:s := Format('Id="rId%d" Target="%s" TargetMode="External" Type="%s"', [rId_Hyperlink, target, SCHEMAS_HYPERLINK]);Problem
The
targetstring (a URL from a hyperlink) is inserted directly into the XML attribute without escaping ampersands. URLs with query parameters (e.g.,?param=1¶m=2) produce invalid XML:Fix
s := Format('Id="rId%d" Target="%s" TargetMode="External" Type="%s"', [rId_Hyperlink, UTF8TextToXMLText(target), SCHEMAS_HYPERLINK]);Existing utility function
Both fixes use
UTF8TextToXMLText()from thefpsxmlcommonunit, which is already in theusesclause ofxlsxooxml.pas. This function correctly escapes&→&,<→<,>→>,"→",'→', and avoids double-escaping existing entities.Reproduction
"Google Sans")&(e.g.,https://example.com/page?a=1&b=2).xlsxopenpyxl.load_workbook("file.xlsx")Result:
lxml.etree.XMLSyntaxError: attributes construct errorWorkaround (Python)
Impact
High — any application that combines fpspreadsheet-generated
.xlsxfiles with strict XML parsers (openpyxl, lxml, .NET Open XML SDK) will fail to load them. This affects data pipelines, automated processing, and interoperability with the Python data ecosystem.