Where
-Infinity
0
Severity
7.5
Integer Overflow, Out-of-bounds Read
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Summary

ColumnNameToNumber (lib.go:220-237) accumulates the bijective base-26 value of a column name in an int64 with only an upper-bound check (col > MaxColumns) applied after the loop and no overflow detection. A 14-letter column name whose true value is 3·2⁶⁴ — e.g. VGWQHXLSDVIKWV — wraps to col = 0 and passes the guard, so CellNameToCoordinates("VGWQHXLSDVIKWV1") returns (col=0, row=1, err=nil).

When normalizing a r="0" row, checkSheetR0 (excelize.go:417-443, called from checkSheet at excelize.go:405) runs its checkRow closure with col = 0: colIdx := col - 1 becomes -1, and sheetData.Row[rowIdx].C[-1] (excelize.go:424) raises an unrecovered panic: runtime error: index out of range [-1], killing the host process.

Details

- The row side of the same coordinate gate is enforced (checkRowNum at excelize.go:342-350 bounds r before the make([]xlsxRow, row) allocation; this includes the fix for GHSA-h69g-9hx6-f3v4 / CVE-2026-54063, present in the audited commit). The column side is not: checkSheet/lastRowNum/checkSheetR0 treat err == nil from CellNameToCoordinates as proof of an in-domain coordinate (excelize.go:356, :438), which is unsound because of the wrap-around above. - The same unsound gate also feeds ws.SheetData.Row[rowIdx].C[colNum-1] in xlsxWorksheet.checkRow (rows.go:969): a row combining a valid large column (e.g. XFD1) with an overflowed column panics identically. - Reachable from any workSheetReader-based API on an attacker-supplied worksheet: GetCellValue, GetCellFormula, SetCellValue, GetMergeCells, GetSheetDimension, GetColWidth, AddTable, etc. - Probe on pristine master: ColumnNameToNumber("VGWQHXLSDVIKWV") returns (0, nil). - This is a distinct root cause from GHSA-h69g-9hx6-f3v4 (row-index allocation): different mechanism (int64 wrap-around → negative index, not oversized allocation), different sink, different fix.

PoC

A standalone program (public API only) was provided to the maintainer by email (3-column-overflow): it builds a workbook in memory whose xl/worksheets/sheet1.xml contains <sheetData><row r="0"><c r="VGWQHXLSDVIKWV1" t="inlineStr"><is><t>pwn</t></is></c></row></sheetData> (<1 KB of attacker XML), calls OpenReader, then GetCellValue("Sheet1", "A1") → PANICREPRODUCED: runtime error: index out of range [-1] on master ecd99d761fe0 (2026-09-08). With the proposed patch the same program prints NOPANICBLOCKED.

Impact

A <1 KB crafted .xlsx crashes any service that opens a user-supplied spreadsheet and reads it — upload processing, mail-scanning pipelines, spreadsheet conversion endpoints. Remote, unauthenticated, no privileges.

Proposed fix

Bound the accumulated value inside the loop: check col > MaxColumns after each digit. Every digit is at least 1, so any name whose true value exceeds MaxColumns crosses the bound inside the loop, before the accumulation can wrap or overflow — this provably covers all cases, including wraps that would land back inside [1, MaxColumns]. A complete patch has been provided to the maintainer.

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Excelize is a Go language library for reading and writing Microsoft Excel spreadsheets. From 2.3.1 to 2.11.0, agile decryption accepts an attacker-controlled spinCount and performs that many password-key derivation iterations before verifier validation. OpenFile reaches agileDecrypt, which passes the unbounded spinCount to convertPasswdToKey before password verification. When a crafted OLE encrypted-workbook header supplies an excessive spinCount and the file is opened, the key-derivation loop performs unbounded attacker-selected work and cannot be cancelled, allowing an attacker to consume a CPU core for an attacker-controlled duration. No fixed version is available as of this review.

1 / 2
Source: MITRE
First published (updated )
Severity
6.5
Out-of-bounds Read
AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H

Summary

A worksheet containing <mergeCell ref=""/> makes mergeCellsParser leave the cached rectangle empty, and cellInRange then indexes that empty slice without a length check. Every non-streaming cell API panics with index out of range [0] with length 0 on the first cell read after opening the file.

Where it is

cell.go, cellInRange

func cellInRange(cell, ref []int) bool { return cell[0] >= ref[0] && cell[0] <= ref[2] && cell[1] >= ref[1] && cell[1] <= ref[3] }

ref is indexed at four positions with no bounds check.

The caller that can hand it an empty slice, mergeCellsParser

if ref := ws.MergeCells.Cells[i].Ref; len(ws.MergeCells.Cells[i].rect) == 0 && ref != "" { if strings.Count(ref, ":") != 1 { ref += ":" + ref } rect, err := rangeRefToCoordinates(ref) if err != nil { return cell, err } = sortCoordinates(rect) ws.MergeCells.Cells[i].rect = rect } if cellInRange([]int{col, row}, ws.MergeCells.Cells[i].rect) {

The ref != "" condition means an empty ref skips the block that populates rect, so rect stays nil. The cellInRange call on the next line is unconditional and receives that nil slice.

Ref comes straight from xl/worksheets/sheetN.xml through encoding/xml, so an empty string is entirely attacker-controlled.

Impact

Any consumer that opens an untrusted workbook and reads a cell panics. The loop scans every merged cell, so any cell reference triggers it, not a specific one. Affected entry points include GetCellValue, GetCellType, GetCellFormula, SetCellValue and the in-cell branch of GetPictures. The streaming Rows and GetRows use the SAX path and do not go through this parser, and GetMergeCells routes through Rect() which errors cleanly, which is probably why this has not surfaced before.

There is no option or flag involved; opening the file succeeds and the panic fires on the first cell read. Unless the caller wraps the call in recover() it takes the process down.

This is a regression. Commit a34c81e (PR #1500, 2023-03-20) replaced checkCellInRangeRef, whose len(rng) != 2 guard returned cleanly for an empty ref, with the cached-rect fast path above, and the guard did not come along. git merge-base --is-ancestor confirms that commit is an ancestor of v2.11.0, so released versions are affected as well as HEAD.

Proof of concept

Executed at HEAD.

Build a minimal xlsx whose xl/worksheets/sheet1.xml contains:

<mergeCells count="1"><mergeCell ref=""></mergeCell></mergeCells>

Then:

f, err := excelize.OpenReader(bytes.NewReader(data)) if err != nil { t.Fatal(err) } , = f.GetCellValue("Sheet1", "A1")

Observed, running against the repository at HEAD:

panic: runtime error: index out of range [0] with length 0 excelize.cellInRange cell.go:1691 excelize.(xlsxWorksheet).mergeCellsParser cell.go:1660 excelize.(File).getCellStringFunc cell.go:1512 excelize.(File).GetCellValue cell.go:72

OpenReader itself returns no error; the file is 1656 bytes. A1, B1 and A2 all reproduce it.

For context on how targeted this is, I ran a battery of 38 crafted files covering data validation, conditional formatting, cell and row references, number formats, cols, hyperlinks, dimension, shared strings and tables. Only the empty-ref merge cell panicked; everything else returned a clean error. The other parsing paths look well guarded.

Suggested fix

Skip the entry when the rectangle is empty, before the range test:

if len(ws.MergeCells.Cells[i].rect) == 0 { continue }

Credit goes to arpitjain099.

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
Out-of-bounds Read, Null Pointer Dereference
AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H

Summary

GetConditionalFormats reads sub-elements of a <cfRule> straight out of xl/worksheets/sheetN.xml and indexes them without checking length, and in one case without checking for nil. Three rule types are affected: cellIs, dataBar and colorScale. A workbook with a rule that is missing a child a real Excel file would always have panics the call.

Where it is

All three sinks are in styles.go, at the same line numbers in v2.11.0 and on master d552a7e. All three are reached from GetConditionalFormats through styles.go:3271.

styles.go:3003, in extractCondFmtCellIs:

go format.Value = c.Formula[0]

The branch above it handles len(c.Formula) == 2; this one is the fallback and does not check that there is a formula at all, so a cellIs rule with no <formula> child indexes an empty slice.

styles.go:3132, in the colorScale extractor:

go values := len(c.ColorScale.Cfvo)

c.ColorScale is a xlsxColorScale and is nil when the <cfRule type="colorScale"> element has no <colorScale> child. Lines 3148 and 3153 then index Cfvo[1] and Cfvo[2] in the three-colour branch with no length check either.

styles.go:3186, :3188 and :3190, in the dataBar extractor:

go format.MinType = c.DataBar.Cfvo[0].Type ... format.BarColor = "#" + f.getThemeColor(c.DataBar.Color[0])

The guard here is c.DataBar != nil, which says nothing about the length of Cfvo or Color, so an empty <dataBar></dataBar> element reaches all three.

Who the attacker is

Anyone who can hand a spreadsheet to a service that opens it and calls GetConditionalFormats. No authentication, no user interaction beyond the service doing its normal job, and the file is small.

Reproduction

Three minimal .xlsx files were built, each a real zip with [ContentTypes].xml, rels/.rels, xl/workbook.xml, xl/rels/workbook.xml.rels and one worksheet, opened each with the public excelize.OpenReader and called GetConditionalFormats("Sheet1"). Nothing internal is touched.

The worksheet fragment for the cellIs case, note there is no <formula> child:

xml <conditionalFormatting sqref="A1"><cfRule type="cellIs" operator="equal" priority="1" dxfId="0"/></conditionalFormatting>

for dataBar:

xml <conditionalFormatting sqref="A1"><cfRule type="dataBar" priority="1"><dataBar></dataBar></cfRule></conditionalFormatting>

and for colorScale:

xml <conditionalFormatting sqref="A1"><cfRule type="colorScale" priority="1"/></conditionalFormatting>

Observed against master d552a7e on go1.26.5:

text cellIs panic: runtime error: index out of range [0] with length 0 styles.go:3003 styles.go:1217 styles.go:3271

dataBar panic: runtime error: index out of range [0] with length 0 styles.go:3186 styles.go:1262 styles.go:3271

colorScale panic: runtime error: invalid memory address or nil pointer dereference styles.go:3132 styles.go:1259 styles.go:3271

One honesty note on impact. These are ordinary Go panics, not fatal errors, so a caller that wraps the call in recover survives them.

Suggested fix

Length-check c.Formula before line 3003 and return the rule with an empty value when there is no formula. Nil-check c.ColorScale before line 3132 and length-check ColorScale.Cfvo before indexing 1 and 2. Length-check DataBar.Cfvo and DataBar.Color alongside the existing nil check at 3186 to 3190. Skipping the malformed rule rather than erroring would keep GetConditionalFormats usable on files that are merely sloppy.

Affected versions

github.com/xuri/excelize/v2 up to and including v2.11.0, and master at d552a7e. I read the three sinks at tag v2.11.0 and at master and ran the reproducers against master.

1 / 2
Source: GitHub
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203