XSSF: validate row/column/cell index before mutating the sheet XML - #1825
ken-swyfft wants to merge 4 commits into
Conversation
XSSFSheet.CreateRow adds the row element to sheetData before XSSFRow's RowNum setter validates the index. When the setter rejects the index the element is already in the XML, and because _rows is only updated after validation the row is orphaned: invisible to GetRow and PhysicalNumberOfRows, but still written to the file. The result is a workbook Excel refuses to open. Each leaked row carries the same r attribute — XSSFRow's constructor fills in LastRowNum + 2 for an unset r before the setter throws — so the rows are duplicated and precede the real ones, breaking the required ascending order. Excel reports "We found a problem with some content" and strips the sheet's data when repairing it. A caller only has to probe a name that doesn't resolve and catch the exception, which is what "does this optional cell exist?" code does, for the saved file to be corrupt. XSSFSheet.CreateColumn and XSSFRow.CreateCell have the same mutate-then-validate shape. CreateColumn leaves a col definition for a column that was never created; the orphaned cell does not reach the file today only because XSSFRow.OnDocumentWrite rebuilds the row's cell array when the counts disagree. Hoist the checks so nothing is written until the index is known good. The peers already do it this way — SXSSFSheet.CreateRow, SXSSFRow .CreateCell, XSSFColumn.CreateCell and XSSFRow.GetCell all validate first, as does HSSFSheet.CreateRow — so this makes XSSF consistent with the rest of the library. Exception types and messages are unchanged.
…xception type
- GetWrittenSheetXml opens the package read-only, disposes the stream and
drops RegexOptions.Compiled from a single-use regex.
- Match the file's 'for (' spacing.
- XSSFCell.CheckBounds documented as throwing ArgumentException, not
RuntimeException (a Java leftover on the line this patch touched).
|
Two updates. Pushed a follow-up commit tidying the new test helper — the package is now opened read-only, the The red CI here is unrelated to this change — restore fails before compile on the (Comment from Claude, an AI assistant, on behalf of @ken-swyfft.) |
The problem
XSSFSheet.CreateRowadds the row element tosheetDatabefore the index is validated:When
RowNumrejects the index the element is already in the XML, and because_rowsis only updated afterwards the row is orphaned — invisible toGetRowandPhysicalNumberOfRows, but still written to the file.The result is a workbook Excel refuses to open. This is enough to produce one:
xl/worksheets/sheet1.xml:Every leaked row carries the same
r—XSSFRow's constructor fills inLastRowNum + 2for an unsetrbefore the setter throws — so the rows are both duplicated and ahead of the real ones, breaking the required ascending order. Excel reports "We found a problem with some content" and strips the sheet's data when repairing it.Two things make this hard to notice. NPOI re-reads the malformed file without complaint (
PhysicalNumberOfRowscomes back as 4), so a write-out/read-back round trip looks fine — only the raw part XML shows the damage. And the caller doesn't have to do anything unusual: catching the exception and carrying on is exactly what "does this optional cell exist?" probing code does. We hit this in production, where a workbook generator probed named cells that some templates lack; every failed probe added another phantom row, and the delivered spreadsheets opened as corrupt.XSSFSheet.CreateColumnandXSSFRow.CreateCellhave the same mutate-then-validate shape.CreateColumnleaves acoldefinition for a column that was never created. The orphaned cell doesn't reach the file today, but only becauseXSSFRow.OnDocumentWriterebuilds the row's cell array when the counts disagree — the object model is still left inconsistent after the throw.The change
Hoist the bounds checks so nothing is written until the index is known good.
This makes XSSF consistent with the rest of the library rather than introducing a new convention —
SXSSFSheet.CreateRow,SXSSFRow.CreateCell,XSSFColumn.CreateCellandXSSFRow.GetCellall validate first, as doesHSSFSheet.CreateRow. The row check is copied fromSXSSFSheet.CreateRow, andXSSFRow.CreateCellnow calls the existingXSSFCell.CheckBounds(widened fromprivatetointernal) instead of reaching it indirectly viaSetCellNumafter the element has been added.Exception types and messages are unchanged, so the existing
BaseTestRow.BaseTestRowBounds/BaseTestCellBoundsassertions still hold — only the side effect of the throw is gone.Tests
Four tests in
TestXSSFSheetandTestXSSFRow:CT_counts are what has to be checked);Expected: "1,2,3" But was: "4,4,4,1,2,3"and dumps the offending XML.Verified on
master: the fullNPOI.OOXML.TestCasessuite passes (1854 tests, 0 failures) on net10.0, and the new tests pass on net472 as well.Submitted by Claude (an AI assistant) on behalf of @ken-swyfft. The analysis, patch and tests were produced by Claude and reviewed before submission.