Repository navigation
chore(renovate): Security update Update module github.com/xuri/excelize/v2 to v2.11.1 [SECURITY] - #390
Open
renovate[bot] wants to merge 1 commit into
Open
chore(renovate): Security update Update module github.com/xuri/excelize/v2 to v2.11.1 [SECURITY]#390renovate[bot] wants to merge 1 commit into
renovate[bot] wants to merge 1 commit into
Conversation
Contributor
Author
ℹ️ Artifact update noticeFile name: go.modIn order to perform the update(s) described in the table above, Renovate ran the
Details:
|
renovate
Bot
force-pushed
the
renovate/go-github.com-xuri-excelize-v2-vulnerability
branch
2 times, most recently
from
November 7, 2026 00:44
d653dcd to
eee1bd8
Compare
…ze/v2 to v2.11.1 [SECURITY]
renovate
Bot
force-pushed
the
renovate/go-github.com-xuri-excelize-v2-vulnerability
branch
from
November 8, 2026 04:38
eee1bd8 to
c1b018b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
This PR body was truncated due to platform limits.
This PR contains the following updates:
v2.11.0→v2.11.1Excelize: GetStyle panics on a negative fillId, borderId or fontId in styles.xml
CVE-2026-107225 / GHSA-5h23-36rv-pm65
More information
Details
Summary
File.GetStyleindexes the fill, border and font tables with values taken straight out ofxl/styles.xml, and the conditions gating those lookups check only the upper bound. A workbook whosecellXfsentry carriesfillId="-1",borderId="-1"orfontId="-1"reaches a negative slice index and panics. excelize has norecover(), so the panic leavesGetStyleand takes the calling process with it.Same defect class as GHSA-48hm-4h8j-58fg, the negative shared-string index, in a different file. That one was fixed by adding the missing lower bound; these three sites still lack it.
Where it is
styles.go, inGetStyle, lines 1683, 1686 and 1689:The conditions, at lines 1171 to 1185, bound only the top:
*xf.FillID < len(...)is satisfied by any negative value.FillID,BorderIDandFontIDare*intunmarshalled directly from thefillId,borderIdandfontIdattributes, so the value is whatever the file says. The border and font conditions have the same shape.The correct pattern is already in the same function, six lines above at 1677:
and again at 1783. So the style index itself is guarded on both sides while the three ids it leads to are not.
Proof of concept
Executed against
masteratf98df08, which is the merge of #2366, so this is current rather than historical. The harness builds a normal styled workbook with excelize, rewrites one attribute of thecellXfsentry inside the zip to-1, reopens it and callsGetCellStylethenGetStyle:The control confirms the harness reads a valid file correctly, so the three panics are the negative ids rather than a broken fixture. Worth mentioning because my first attempt at this harness patched only
fillIdand appeared to show the other two were fine; they are not, the replacement had simply missed them.Impact
Any application that opens an untrusted
.xlsxand reads cell styling crashes.GetStyleis reached throughGetCellStyleand from the style-copying and rendering paths, so it sits on the ordinary read path. With norecover()anywhere in excelize, a CLI or worker exits and a service returns 500 per request unless the caller installed its own recovery middleware.No memory corruption and no information disclosure. Availability only.
Suggested fix
Add the lower bound to each of the three conditions, matching what line 1677 already does for the style index:
and the same for
BorderIDandFontID. Three one-line changes inextractStyleCondFuncs.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Excelize: A Zip64 uncompressed-size of 2^63 panics OpenFile/OpenReader
CVE-2026-107224 / GHSA-fw94-4wwp-w8pw
More information
Details
Summary
A Zip64 uncompressed-size of 2^63 casts to a negative int64 that bypasses the unzip-size guard and reaches make([]byte, 0, negativeCap)
A Zip64 uncompressed-size in the range [2^63, 2^64) casts to a negative
int64inzip.File.FileInfo().Size(), andReadZipReaderdoes signed arithmetic on that value, so the total-decompression guard is bypassed and the negative size reachesmake([]byte, 0, size)inreadFile. A 159-byte crafted file panicsOpenFileandOpenReaderwithruntime error: makeslice: cap out of range.The guard, at
lib.go:44-46on HEADae2113b:FileInfo().Size()returnsint64(UncompressedSize64).UncompressedSize64is read from the Zip64 extended-information extra record in the central directory and is fully attacker controlled, so any value with the top bit set arrives as a negativeint64.unzipSizethen goes negative and the comparison againstUnzipSizeLimit(default1000 << 24,templates.go:193) is false no matter how many such entries the archive contains.The same negative value also fails the two stream-to-temp checks at
lib.go:53andlib.go:64(fileSize > f.options.UnzipXMLSizeLimit), so the entry is not diverted to a temp file and control reachesreadFile:makewith a negative capacity panics. There is norecover()in any non-test file in the library, so the panic leaves the public API and takes down the calling goroutine.The asymmetry inside this same function is the clearest way to see it:
unzipToTemp(lib.go:83) handles an entry with a plainio.Copyand never pre-allocates from the declared size, so it is indifferent to whatever the header claims. Only the pre-allocating branch trusts that number, and it trusts it after a signed comparison that a wrapped value walks straight through.Reproduction, executed against HEAD
ae2113b(go1.26.5)A 159-byte archive was built with one stored entry named
xl/worksheets/sheet1.xml: truthful 1-byte local header, central-directory 32-bit uncompressed size set to the0xFFFFFFFFZip64 sentinel, and a Zip64 extra record (tag0x0001, data size 8) declaringUncompressedSize64 = 0x8000000000000000.Control, the identical archive with
UncompressedSize64 = 0x7FFFFFFFFFFFFFFF:The control isolates the defect to the sign flip rather than to size magnitude: the guard behaves correctly for any positive declared size and fails only once the value wraps.
archive/zipitself accepts the archive without complaint, so nothing upstream of excelize rejects it.What I verified and what I did not
I ran both cases above myself and confirmed each cited line at HEAD. I caught the panic with a deliberate
recoverin my harness so I could print it; nothing in excelize recovers it, which I checked by grepping every non-test.gofile. I did not separately run theOpenFilevariant, sinceOpenFilereaches the sameReadZipReaderloop, and I did not attempt to turn the bypassed size cap into a separate decompression-bomb primitive.Attacker model
Any service that opens a spreadsheet it did not produce. The payload is 159 bytes, needs no authentication, and is deterministic.
Suggested fix
Compare against the limit in unsigned space, and bound the allocation independently rather than trusting the header:
and in
readFile, reject or clamp a declared size that is negative or larger than the limit before callingmake. The same unsigned treatment applies to the twoUnzipXMLSizeLimitcomparisons, which currently route a wrapped size away from the safe streaming path and into the pre-allocating one.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Excelize: Unbounded attribute is loaded with no MaxColumns check and expanded per-column by flatCols(), so any column mutator hangs or OOMs the process
CVE-2026-107223 / GHSA-fq3v-74gv-27gm
More information
Details
Summary
The
maxattribute of a<col>element inxl/worksheets/sheetN.xmlis read verbatim on open with no check againstMaxColumns, andflatCols()then walksMin..Maxdoing adeepcopy.Copyandappendper iteration. Executed:max="300000", only about 18x past the real 16384 limit, cost 46.2 s of CPU and 122 MB on a singleSetColWidthcall, and the growth is linear inmaxup to 2^31-1.Confirmed at
ae2113b(HEAD at the time of audit).Details
col.go:547,flatCols(), with the two loops at:549and:564:column.Minandcolumn.Maxare plain int attributes onxmlWorksheet.go:279-280, bound straight from the file.MaxColumnsis 16384 (templates.go:176) and nothing in the parse path compares against it.The contrast that makes this an oversight rather than a design decision is inside the callers themselves.
SetColWidthvalidates its own column argument throughparseColRange, which clamps toMaxColumns. ButflatColsiteratesws.Cols.Col, the file-loaded slice, so the caller's validated argument never constrains the loop. The crafted column expands no matter which column the caller touches.The row axis has the guard this axis is missing: GHSA-h69g-9hx6-f3v4 capped
checkSheet's row allocation atTotalRows, and GHSA-q5j5-6p94-4gwc capped streamingRows.Next. There is no column-axis equivalent.Public entry points that flatten the existing columns:
SetColWidth(col.go:498into:534),SetColStyle(col.go:431into:477),SetColVisible(col.go:282into:313),SetColOutlineLevel(col.go:376into:407).PoC
Executed in-package: crafted a real xlsx, rezipped with an injected
<cols>block before<sheetData>, opened it withOpenReader, then calledSetColWidth.Measured:
max="300000": 46.2 s CPU, 122 MB allocated,ws.Cols.Colgrew to 300000 entries, from one call.max="5000000": did not complete in 110 s, killed by the test timeout.Growth is linear in
max. Atmax=2147483647that is roughly two billionxlsxColallocations, which is hundreds of gigabytes and in practice a permanent hang ending in OOM.Impact
Any service that opens an untrusted spreadsheet and calls a column mutator. The input is a few dozen bytes of XML inside an otherwise ordinary workbook, no authentication is involved beyond whatever gates the upload, and the process is either wedged for minutes or killed by the OOM reaper. Availability only; no read or write primitive here.
Suggested fix
Validate
col.Minandcol.MaxagainstMinColumns/MaxColumnswhen parsing the<col>element, or at the top offlatCols, returningErrColumnNumberfor out-of-range values. That mirrors whatcheckRowNumalready does on the row axis.Why this is not GHSA-h69g-9hx6-f3v4 or GHSA-q5j5-6p94-4gwc
Both of those are row-axis allocation bounds, and both fixes cap at
TotalRows. Neither touched<col>parsing orflatCols. This is the same weakness class on the column axis, and it is arguably worse than the two that were fixed, because those had a cap that was bypassed while this one has no cap at all.Severity
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:H/SC:N/SI:N/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Excelize: GetConditionalFormats indexes conditional-formatting rule sub-elements with no length or nil check
CVE-2026-107222 / GHSA-rxcj-4pj5-74gr
More information
Details
Summary
GetConditionalFormatsreads sub-elements of a<cfRule>straight out ofxl/worksheets/sheetN.xmland indexes them without checking length, and in one case without checking for nil. Three rule types are affected:cellIs,dataBarandcolorScale. 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 masterd552a7e. All three are reached fromGetConditionalFormatsthroughstyles.go:3271.styles.go:3003, inextractCondFmtCellIs: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 acellIsrule with no<formula>child indexes an empty slice.styles.go:3132, in the colorScale extractor:c.ColorScaleis a*xlsxColorScaleand is nil when the<cfRule type="colorScale">element has no<colorScale>child. Lines 3148 and 3153 then indexCfvo[1]andCfvo[2]in the three-colour branch with no length check either.styles.go:3186,:3188and:3190, in the dataBar extractor:The guard here is
c.DataBar != nil, which says nothing about the length ofCfvoorColor, 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
.xlsxfiles were built, each a real zip with[Content_Types].xml,_rels/.rels,xl/workbook.xml,xl/_rels/workbook.xml.relsand one worksheet, opened each with the publicexcelize.OpenReaderand calledGetConditionalFormats("Sheet1"). Nothing internal is touched.The worksheet fragment for the
cellIscase, note there is no<formula>child:for
dataBar:and for
colorScale:Observed against master
d552a7eon go1.26.5:One honesty note on impact. These are ordinary Go panics, not fatal errors, so a caller that wraps the call in
recoversurvives them.Suggested fix
Length-check
c.Formulabefore line 3003 and return the rule with an empty value when there is no formula. Nil-checkc.ColorScalebefore line 3132 and length-checkColorScale.Cfvobefore indexing 1 and 2. Length-checkDataBar.CfvoandDataBar.Coloralongside the existing nil check at 3186 to 3190. Skipping the malformed rule rather than erroring would keepGetConditionalFormatsusable on files that are merely sloppy.Affected versions
github.com/xuri/excelize/v2up to and including v2.11.0, and master atd552a7e. I read the three sinks at tag v2.11.0 and at master and ran the reproducers against master.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Excelize: a row whose earlier cell has a higher column reference than its last cell panics index out of range on almost every worksheet read API
CVE-2026-107221 / GHSA-8mcq-6wmr-jrjv
More information
Details
Affected versions and vulnerable location
ae2113b(current HEAD).rows.go:967ws.SheetData.Row[rowIdx].C[colNum-1] = *colDatainsidecheckRow.checkRowcomputeslastColfrom the column of the last cell in document order (rows.go:940), allocatestargetListof that length, then re-scatters every source cell intoC[colNum-1].Root cause
The slice is sized from the last cell's column, but cells are not required to be column-sorted in the XML. A cell that appears earlier in the row but references a higher column than the last cell has
colNum-1 >= len(targetList), so the assignment writes out of range.MaxColumns/TotalRowsdo not help, every individual column is valid; the bug is the ordering assumption, not magnitude.Attacker model and reachability
Any service that opens an untrusted spreadsheet and calls a worksheet API that goes through
workSheetReader -> checkRow(excelize.go:332):GetCellValue,GetCellFormula,CalcCellValue,GetMergeCells,SetCellValue, and essentially every non-streaming worksheet call. (The streamingGetRows/Rows()SAX path does not trigger it.) Unauthenticated, deterministic, unrecovered panic -> process crash.Proof of concept (executed)
Crafted
xl/worksheets/sheet1.xmlwith a row whose cells are out of column order and whose earlier cell exceeds the last cell's column:GetCellValue("Sheet1","A1")(viagetCellStringFunc -> workSheetReader -> checkRow) panickedindex out of range [3] with length 3atrows.go:967.Confirming grep:
rg -n "func checkRow|lastCol|Row\[rowIdx\].C\[colNum-1\]" rows.goSuggested fix
Size
targetListfrom the maximum cell column in the row (not the last cell in document order), or bounds-checkcolNum-1againstlen(targetList)and grow the slice as needed before the assignment.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Excelize: Panic in cellInRange on a worksheet with an empty mergeCell ref
CVE-2026-107220 / GHSA-g27h-8qhm-6pff
More information
Details
Summary
A worksheet containing
<mergeCell ref=""/>makesmergeCellsParserleave the cached rectangle empty, andcellInRangethen indexes that empty slice without a length check. Every non-streaming cell API panics withindex out of range [0] with length 0on the first cell read after opening the file.Where it is
cell.go,cellInRangerefis indexed at four positions with no bounds check.The caller that can hand it an empty slice,
mergeCellsParserThe
ref != ""condition means an emptyrefskips the block that populatesrect, sorectstays nil. ThecellInRangecall on the next line is unconditional and receives that nil slice.Refcomes straight fromxl/worksheets/sheetN.xmlthroughencoding/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,SetCellValueand the in-cell branch ofGetPictures. The streamingRowsandGetRowsuse the SAX path and do not go through this parser, andGetMergeCellsroutes throughRect()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, whoselen(rng) != 2guard returned cleanly for an empty ref, with the cached-rect fast path above, and the guard did not come along.git merge-base --is-ancestorconfirms 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.xmlcontains:Then:
Observed, running against the repository at HEAD:
OpenReaderitself 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:
Credit goes to arpitjain099.
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Excelize: Unbounded spinCount in agile decryption burns CPU during OpenFile
CVE-2026-107219 / GHSA-jrfj-fhj2-jjvm
More information
Details
Opening a file whose first eight bytes are the OLE magic number sends excelize down the decryption path whether or not the caller set a password, or supports encrypted workbooks at all.
openReaderAt(excelize.go:198-215) branches on the header alone, andagileDecryptcallsconvertPasswdToKeybefore the verifier hash is checked, so the key-derivation loop runsspinCounttimes regardless.spinCount(crypt.go:98) is a plainintfilled by a barexml.Unmarshalof the file's own EncryptionInfo stream. Nothing bounds it.A 3072-byte file with spinCount 100000000 makes
OpenFiletake 58.65s on v2.11.0 with default options, then returnzip: not a valid zip file. It is linear at about 0.6 microseconds per iteration and the attacker picks the number, so 1e9 is roughly ten minutes. Nothing on the path takes acontext.Context, so the caller cannot cancel it; in an HTTP handler the write timeout returns a response while the goroutine keeps spinning. Memory stays flat at 24 MB, so nothing reclaims it either.The loop arrived with
crypt.goin v2.3.1 and is unchanged through v2.11.0.Excel and LibreOffice write spinCount 100000, which costs 61ms here, so a ceiling well above the legitimate value would cost real files nothing.
This is availability only, and it is not a vulnerability for a program that only opens files its own operator produced.
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Excelize: RIGHT() on supplementary-plane text slices with a negative index and panics
CVE-2026-107218 / GHSA-8jjq-8j9w-m2v6
More information
Details
leftRight(calc.go:14236) tests the length withcountUTF16String, which counts a rune above U+FFFF as 2. The RIGHT branch on 14241 then slices[]rune(text)atutf8.RuneCountInString(text)-numChars, which counts it as 1. For text with N such runes the two measures are 2N and N, so anynumCharsbetween them passes the guard and gives a negative index. ThenumChars < 0check at 14216 does not help, since the value that gets through is positive.What makes it worth reporting is
AutoFitColWidth, which evaluates formulas without looking like it does, so normalising an uploaded sheet is enough to reach it. One scoping correction to my own wording there:AutoFitColWidthwas added in v2.11.0 and does not exist at v2.10.1, so on v2.10.1 the only reachable path is an explicitCalcCellValue.A 6,077-byte file containing two U+1D7D9 characters in A1 and RIGHT(A1,3) in B1 was created and then opened in a separate program without recovery enabled:
So it is a regression, not an old defect. Commit a880146 (2026-01-16) moved the guard to
countUTF16Stringand left the slice on the rune index, andgit tag --containsgives v2.10.1 and v2.11.0 only.RIGHT only. RIGHTB reaches the same function but takes the byte branch at 14225, which is internally consistent, and I probed MID and MIDB from 1 to 5 with no panic. It is the same negative-index family as GHSA-fx5j-qcqg-grpf and GHSA-48hm-4h8j-58fg, though those are shared-string lookups in the reader rather than a unit mismatch in the formula library.
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:LReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Excelize ColumnNameToNumber: int64 overflow yields an out-of-domain coordinate with nil error, causing negative slice index panic on r="0" rows
CVE-2026-107217 / GHSA-c85p-xxjj-2r75
More information
Details
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 tocol = 0and passes the guard, soCellNameToCoordinates("VGWQHXLSDVIKWV1")returns(col=0, row=1, err=nil).When normalizing a
r="0"row,checkSheetR0(excelize.go:417-443, called fromcheckSheetat excelize.go:405) runs itscheckRowclosure withcol = 0:colIdx := col - 1becomes -1, andsheetData.Row[rowIdx].C[-1](excelize.go:424) raises an unrecoveredpanic: runtime error: index out of range [-1], killing the host process.Details
checkRowNumat excelize.go:342-350 boundsrbefore themake([]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/checkSheetR0treaterr == nilfromCellNameToCoordinatesas proof of an in-domain coordinate (excelize.go:356, :438), which is unsound because of the wrap-around above.ws.SheetData.Row[rowIdx].C[colNum-1]inxlsxWorksheet.checkRow(rows.go:969): a row combining a valid large column (e.g.XFD1) with an overflowed column panics identically.workSheetReader-based API on an attacker-supplied worksheet:GetCellValue,GetCellFormula,SetCellValue,GetMergeCells,GetSheetDimension,GetColWidth,AddTable, etc.ColumnNameToNumber("VGWQHXLSDVIKWV")returns(0, nil).PoC
A standalone program (public API only) was provided to the maintainer by email (
3-column-overflow): it builds a workbook in memory whosexl/worksheets/sheet1.xmlcontains<sheetData><row r="0"><c r="VGWQHXLSDVIKWV1" t="inlineStr"><is><t>pwn</t></is></c></row></sheetData>(<1 KB of attacker XML), callsOpenReader, thenGetCellValue("Sheet1", "A1")→PANIC_REPRODUCED: runtime error: index out of range [-1]on masterecd99d761fe0(2026-09-08). With the proposed patch the same program printsNO_PANIC_BLOCKED.Impact
A <1 KB crafted
.xlsxcrashes 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 > MaxColumnsafter 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.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Excelize ANCHORARRAY: mutually-referencing array formulas recurse unboundedly via re-entrant CalcCellValue, causing a fatal stack overflow
CVE-2026-107216 / GHSA-wp2g-vpjj-g53r
More information
Details
Summary
ANCHORARRAY(calc.go:15137 on current master) evaluates each cell of the referenced spill range by calling the exportedCalcCellValue, which unconditionally constructs a freshcalcContext— fresh entry marker, fresh iterations map, fullMaxCalcIterationsbudget (calc.go:896-900). The circular-reference control only exists within one context: the entry-exclusion marker and per-ref iteration budget are fields of that single context, and completion-based caching (formulaArgCache/calcRawCache) only ever stores results of evaluations that finish.During a pure cycle no nested evaluation ever finishes, so nothing is ever cached to break the recursion, and each hop re-arms the entire budget. Two ordinary, Excel-legal constructs in an attacker-supplied workbook are enough:
A1(dynamic array formula, refA1:A1):_xlfn.ANCHORARRAY($B$1)B1(dynamic array formula, refB1:B1):_xlfn.ANCHORARRAY($A$1)→
CalcCellValue → calcCellValue → evalInfixExp → parseReference → cellResolver → … → ANCHORARRAY → CalcCellValue → …forever, ending in a fatal, unrecoverable Go runtime error:runtime: goroutine stack exceeds …-byte limit / fatal error: stack overflow. Go stack overflows cannot be recovered — the whole process aborts.Details
CalcCellValuecalls.pivotTable.go:538,picture.go:985/1141,col.go:892), so a service that merely adds a pivot table or a picture over such a workbook dies.MaxCalcIterationsis irrelevant because every hop gets a fresh budget.PoC
A standalone program (public API only) was provided to the maintainer by email (
4-anchorarray-recursion):NewFile+SetCellFormulawithFormulaOpts{Type: array, Ref: A1:A1 / B1:B1}, thenCalcCellValue("Sheet1","A1"). On masterecd99d761fe0(2026-09-08) the process aborts withfatal error: stack overflow; with the proposed patch the cycle terminates normally (CYCLE_TERMINATED) and existing calc tests pass.Impact
An attacker ships a workbook containing the two formulas; any service that evaluates a formula over those cells — directly via
CalcCellValue, or implicitly viaAddPivotTable/AddPicture/ auto-fit — aborts. Remote, unauthenticated, process-fatal, no configuration prevents it.Proposed fix
Evaluate spill-range cells through the current calculation context —
fn.f.cellResolver(fn.ctx, …)instead of the exportedCalcCellValue— so the entry check and iterations gate of the running calculation apply.cellResolverreturns the typed value directly (dropping a string round-trip); anArgEmpty → ""shim preserves the existingToNumberbehavior for empty spill cells, and a fresh-context fallback covers the legacy nil-ctx test paths. A complete patch has been provided to the maintainer.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Excelize: extractPart allocates attacker-controlled, unbounded and negative-sized buffers from CFB directory entries: remote panic / OOM DoS
CVE-2026-107215 / GHSA-x2q3-8cjh-766f
More information
Details
Summary
extractPartallocates directly from the mscfb directory-entry size with no clamping (crypt.go:198, same missing bound atcrypt.go:204):entry.Sizeis the rawstreamSizefield from the attacker-controlled CFB directory entry (mscfb v1.0.8fixFile,file.go:109-120: full uint64 → int64 for major version 4, low uint32 for v3). mscfb only detects short sector chains later, insideFile.Read(file.go:485-489"emergency brake") — i.e. after the allocation. The zip path already enforcesUnzipSizeLimit, and the KDF path bounds work withmaxSpinCount; this path consults no limit at all.Details
Two consequences, both execution-confirmed against pristine master (
ecd99d761fe0, 2026-09-08):streamSize = 0xFFFFFFFFFFFFFFFFyieldsentry.Size == -1→panic: runtime error: makeslice: len out of range, which escapesDecrypt(openReaderAtconverts errors, not panics,excelize.go:210-214) and kills the process.streamSizeup to0xFFFFFFFFforces up to 4 GiB of zeroed allocation from a ~12 KB file — an amplification factor of ~350,000×, trivially repeated per request in memory-limited deployments.Call path:
OpenFile/OpenReader/OpenBytes→openReaderAt(excelize.go:208-215, OLE magic sniff) →Decrypt(crypt.go:145-150) →extractPart(crypt.go:198).PoC
A standalone program (public API only) was provided to the maintainer by email (
2-extractpart-oom): it builds a valid-enough v4 (4096-byte sector) CFB — header (signature, sector shift0x0C,DIFAT[0]=1), one directory sector with Root Entry (typeID 5, child=1) and a stream entryEncryptionInfo(objectType=2, startingSector=endOfChain,streamSize=0xFFFFFFFFFFFFFFFF).mscfb.Newsucceeds,doc.Next()returns the entry, andextractPartcallsmake([]byte, -1)before any chain validation. On masterecd99d761fe0the program printspanic: runtime error: makeslice: len out of range; with the proposed patch it prints BLOCKED. An-oomflag demonstrates the 4 GiB allocation variant withstreamSize = 0xFFFFFFFF.Impact
An attacker uploads any file starting with the OLE signature
D0 CF 11 E0 A1 B1 1A E1(~12 KB is enough) to a service that callsexcelize.OpenFile/OpenReader/OpenByteson it (or callsDecryptdirectly): remote, unauthenticated process crash (hard panic) or forced 4 GiB allocation per request (OOM) with a ~350,000× amplification factor.Proposed fix
Bound the declared stream size before allocating in
extractPart: reject negative sizes and sizes above a sane policy cap (1 GiB is far above any real encrypted-workbook part) withErrWorkbookFileFormat, so malformed CFBs are rejected as errors instead of panicking or OOMing. A complete patch has been provided to the maintainer; verified that the patch blocks the PoC while legitimateEncrypt()output still opens.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Excelize Decrypt: unrecoverable panics on malformed OLE/CFB encrypted workbooks
CVE-2026-107214 / GHSA-2j4c-ffch-9f23
More information
Details
Summary
Any file whose first 8 bytes are the OLE compound-file signature (
D0 CF 11 E0 A1 B1 1A E1) is routed byOpenFile/OpenReader/OpenBytes→openReaderAt→Decrypt. The version dispatch only guaranteeslen(EncryptionInfo) >= 4before handing attacker-controlledEncryptionInfo/EncryptedPackagebuffers tostandardDecrypt/agileDecrypt, and no callee validates structure. Malformed but version-valid content therefore fails as an unrecovered runtime panic instead of an error, terminating the calling process.Details
All panic classes below were execution-confirmed against pristine master (
ecd99d761fe0, 2026-09-08).excelize.go:211-213mapsDecrypterrors toErrWorkbookFileFormat, but panics bypass that path and kill the process.len(EncryptionInfo)4–11[:12]crypt.go:238headerSizeuint32[12:12+headerSize]/ fixed-offset header readscrypt.go:238-249standardEncryptionVerifiercrypt.go:282-295header.KeySize= 0xFFFFFFFF[:536870911]with capacity 48crypt.go:321EncryptedPackagestream missing/short[8:0]crypt.go:268len(EncryptionInfo)4–7[8:4]crypt.go:407<keyEncryptors>[0]with length 0crypt.go:416,433saltValuedecoded length ≠ AES blockcipher.NewCBCDecrypter: IV length must equal block sizecrypt.go:425→512keyData blockSize="0"crypt.go:539Note the asymmetry pinpointing the missing constraint: the agile path already checks
len(EncryptedPackage) >= 8(crypt.go:520-523) but the standard path does not (#5). Existing tests only cover the error paths (short<4bytes →ErrUnknownEncryptMechanism, bad XML, base64 errors), never these panic paths.PoC
Standalone programs (public API only, inputs built in memory) were provided to the maintainer by email:
1-decrypt-panicbuilds seven malformed CFB containers and shows each panic escaping the publicDecryptAPI plus one end-to-endOpenReadercrash. All cases print PANIC on master and BLOCKED with the proposed patch. A regression guard proves legitimate decryption is unaffected: a workbook encrypted with the package's ownEncrypt()still opens through the same code path.(A separate advisory covers the unbounded/negative allocation in
extractPart.)Impact
Any service that calls
OpenFile/OpenReader/OpenByteson untrusted input (upload processing, mail scanning, spreadsheet conversion) can be killed remotely and without authentication by a file of ~100 bytes to ~3 KB. No password is required — panics occur during structural/parameter handling before successful decryption. Site variety means filtering one pattern does not help.Proposed fix
A
recover()boundary inDecryptmapping any panic toErrWorkbookFileFormat(restores the documented error-routing contract; legitimate standard/agile decryption unaffected). A complete patch has been provided to the maintainer; per-site length validation is recommended as defense in depth.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Excelize: Nil-pointer dereference in GetSlicers when a worksheet has extLst present but no drawing element
CVE-2026-107213 / GHSA-r7x5-4969-99p7
More information
Details
Summary
GetSlicers guards only
if ws.ExtLst == nil { return }(slicer.go:816-818) and then immediately dereferencesws.Drawing.RIDwith no nil check on ws.Drawing itself.DrawingandExtLstare two independently optional child elements of in xl/worksheets/sheetN.xml, populated separately by encoding/xml depending on whether each tag is present. I executed a PoC: took a plain NewFile() workbook (no drawing, no slicer), saved it, then injected a bare<extLst></extLst>immediately before</worksheet>in xl/worksheets/sheet1.xml (no other change, no drawing element present) and called GetSlicers. Result:runtime error: invalid memory address or nil pointer dereference, confirmed at the ws.Drawing.RID line via stack trace.Reachability / who can trigger this
Unauthenticated: any application that opens an untrusted .xlsx and calls the public
File.GetSlicers(sheet)API. The trigger is a single empty XML tag with no valid slicer or drawing content required at all, making it an even smaller/simpler payload than candidate 1.Proof of Concept / Reproduction
Method:
git describe=v2.11.0-42-ge81f050, i.e. 42 commits past the v2.11.0 tag). 2) Manually re-read the cited source myself (not trusting the claim): slicer.go:805-820 (GetSlicers) and xmlWorksheet.go:21-72 (xlsxWorksheet struct + xlsxDrawing struct) -- confirmed verbatim, matches claim exactly. 3) Confirmed local toolchain: go version go1.27.0 windows/amd64 (/c/Program Files/Go/bin/go). 4) Built the whole package withgo build ./...from the repo root -- succeeded with zero errors, proving the current checkout compiles cleanly. 5) Ran the pre-existing PoC test file already present in this checkout (slicer_extlst_poc_test.go, TestGetSlicersNilDrawingPanic) viago test -run TestGetSlicersNilDrawingPanic -v .: it builds a benign excelize.NewFile() workbook, saves it, reopens it, loads the raw zip entry xl/worksheets/sheet1.xml, string-replaces</worksheet>with<extLst></extLst></worksheet>(adds nothing else, no anywhere), rebuilds the zip byte-for-byte otherwise unchanged (rezipReplacing helper, plain archive/zip re-encode), reopens the crafted file with excelize.OpenReader, and calls the public GetSlicers("Sheet1") API inside a recover(). 6) For a fully independent, non-test-harness reproduction, I additionally wrote and built my own separate standalone Go module+program from scratch (excelize-nilptr-repro/{go.mod,main.go} in my scratchpad dir) that imports the real, unmodified excelize package as an external library dependency via areplace github.com/xuri/excelize/v2 => ../CVE-Hunt-10k/repos/qax-os__excelizedirective (no copy-pasted/reimplemented function -- the actual library code, used through its real public API from a separate consumer program, simulating "any application that opens an untrusted .xlsx"). This program performs the identical craft-and-open sequence but calls GetSlicers with NO recover() at all. Built withgo build -o repro.exe .(succeeded) and ran./repro.exedirectly, observing an unhandled process crash.Evidence:
SOURCE VERIFICATION (current HEAD, read directly): slicer.go:816-820 is exactly as claimed --
if ws.ExtLst == nil { return slicers, err }followed immediately bytarget := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID)with no nil check on ws.Drawing. xmlWorksheet.go:54Drawing *xlsxDrawingxml:"drawing"and xmlWorksheet.go:64 `ExtLst *xlsxExtLst `xml:"extLst"are both independently-optional pointer fields on xlsxWorksheet, populated only when their respective XML tag is present in the worksheet part -- confirming Drawing and ExtLst are unrelated/independent. getSheetRelationshipsTargetByID(sheet, rID string) string (sheet.go:721) takes a plain string, sows.Drawing.RIDis evaluated (dereferencing the nil pointer) at the call site itself.RUN 1 -- existing repo test,
go test -run TestGetSlicersNilDrawingPanic -v .:=== RUN TestGetSlicersNilDrawingPanic
slicer_extlst_poc_test.go:38: original sheet1.xml: ...... (no drawing, no extLst)
slicer_extlst_poc_test.go:48: tampered sheet1.xml: ...<e