Found by fuzzing: a small file of shared formulas could expand to gigabytes of formula text, and a sheet of many <col> elements made every blank cell scan them all. Shared formulas now expand only for cells that are imported, within 256 MB of text.
- commit
- 627e90e5178c9b8dacc234d94d06c706e498f058
- parent
- b704e10b38d0c7f5f4443b6673ac6b181a9ca274
- author
- Dami <dami.etoile@gmail.com>
| M | internal/ | 1 | + |
| M | internal/ | 21 | ++++++--- |
| M | internal/ | 2 | + |
| M | internal/ | 6 | ++- |
| M | internal/ | 65 | +++++++++++++++++++++--------- |
@@ -97,7 +97,9 @@ func (bk *xlsxBook) importSheet(ctx context.Context, b *builder, i int, report f
9797 }9898 report(r.row.num)9999 }100- bk.importRow(b, r)100+ if err := bk.importRow(b, r); err != nil {101+ return 0, fmt.Errorf("sheet %s: %w", bk.sheets[i].name, err)102+ }101103 last = max(last, r.row.num)102104 }103105 for c := range sheet.MaxCols {@@ -114,7 +116,7 @@ func (bk *xlsxBook) importSheet(ctx context.Context, b *builder, i int, report f
114116 // with neither a value nor a formula only counts for its format, and115117 // only up to the row's last cell with one: formatted blanks after it are116118 // left out.117-func (bk *xlsxBook) importRow(b *builder, r *xlsxSheetReader) {119+func (bk *xlsxBook) importRow(b *builder, r *xlsxSheetReader) error {118120 row := &r.row119121 cells := row.cells120122 if !slices.IsSortedFunc(cells, func(a, b xlsxCell) int { return a.col - b.col }) {@@ -127,7 +129,7 @@ func (bk *xlsxBook) importRow(b *builder, r *xlsxSheetReader) {
127129 }128130 }129131 if lastCol == 0 || !b.fits(sheet.Addr{Col: lastCol - 1, Row: row.num - 1}) && row.num > sheet.MaxRows {130- return132+ return nil131133 }132134 j := 0133135 for col := 1; col <= min(lastCol, sheet.MaxCols); col++ {@@ -152,13 +154,18 @@ func (bk *xlsxBook) importRow(b *builder, r *xlsxSheetReader) {
152154 b.put(a, "", st.format, st.style)153155 continue154156 }155- bk.importCell(b, a, c, st)157+ f, err := r.formula(c)158+ if err != nil {159+ return err160+ }161+ bk.importCell(b, a, c, f, st)156162 }163+ return nil157164 }158165 159-func (bk *xlsxBook) importCell(b *builder, a sheet.Addr, c *xlsxCell, st xlsxStyle) {160- if c.formula != "" {161- b.formula(a, fromExcelFormula(c.formula), st.format, st.style, func() { bk.keep(b, a, c, st) })166+func (bk *xlsxBook) importCell(b *builder, a sheet.Addr, c *xlsxCell, formula string, st xlsxStyle) {167+ if formula != "" {168+ b.formula(a, fromExcelFormula(formula), st.format, st.style, func() { bk.keep(b, a, c, st) })162169 return163170 }164171 if c.value == "" {@@ -31,6 +31,7 @@ type xlsxLimits struct {
3131 strings int // shared strings3232 styles int // cell formats, fonts and number formats, each3333 sheets int34+ shared int64 // bytes of shared formulas expanded3435 }3536 3637 var defaultXLSXLimits = xlsxLimits{@@ -44,6 +45,7 @@ var defaultXLSXLimits = xlsxLimits{
4445 strings: 1 << 24,4546 styles: 1 << 16,4647 sheets: 4096,48+ shared: 256 << 20,4749 }4850 4951 // errXLSXLimit is wrapped by every error for a file past a limit.@@ -173,7 +173,7 @@ func TestXLSXBeyondExcelize(t *testing.T) {
173173 174174 func TestXLSXLimits(t *testing.T) {175175 small := defaultXLSXLimits176- small.part, small.total, small.slack, small.token, small.ratio, small.entries = 1<<20, 2<<20, 64<<10, 64<<10, 100, 50176+ small.part, small.total, small.slack, small.token, small.ratio, small.entries, small.shared = 1<<20, 2<<20, 64<<10, 64<<10, 100, 50, 1<<20177177 bomb := oneSheetParts("", strings.Repeat(`<row><c><v>0</v></c></row>`, 100_000))178178 var rows, text strings.Builder179179 for i := range 60_000 {@@ -205,7 +205,9 @@ func TestXLSXLimits(t *testing.T) {
205205 "absolute name": {mergeParts(kitchenParts(), "/etc/passwd", "x"), "unsafe name", 0},206206 "duplicate": {mergeParts(kitchenParts(), "XL/Workbook.xml", "x"), "twice", 0},207207 "entity": {oneSheetParts("", `<row><c t="inlineStr"><is><t>&bomb;</t></is></c></row>`), "invalid character entity &bomb;", 0},208- "not xml": {oneSheetParts("", `<row><c>`), "closed by </sheetData>", 0},208+ "shared formulas": {oneSheetParts("", `<row r="1"><c r="A1"><f t="shared" ref="A1:A9" si="0">`+strings.Repeat("B1+", 1000)+`1</f></c></row>`+209+ strings.Repeat(`<row><c r="A2"><f t="shared" si="0"/></c></row>`, 400)), "shared formulas expand to more than 1 MB", 0},210+ "not xml": {oneSheetParts("", `<row><c>`), "closed by </sheetData>", 0},209211 } {210212 t.Run(name, func(t *testing.T) {211213 lim := small@@ -6,6 +6,8 @@ import (
66 "io"77 "strconv"88 "strings"9+10+ "github.com/FelineStateMachine/012/internal/sheet"911 )1012 1113 // Excel's own limits on a worksheet. A reference past them is an error,@@ -17,12 +19,12 @@ const (
1719 1820 // xlsxCell is a cell of a worksheet row, as the reader passes it on.1921 type xlsxCell struct {20- col int // from 121- style int // the cell format index (s), 0 when not given22- typ string // the type (t): s, str, inlineStr, b, e, n, d or ""23- value string // the value, a shared string looked up24- formula string // the formula, a shared formula expanded25- hasF bool // the cell has a formula element, even an empty one22+ col int // from 123+ style int // the cell format index (s), 0 when not given24+ typ string // the type (t): s, str, inlineStr, b, e, n, d or ""25+ value string // the value, a shared string looked up26+ f cellFormula // its <f>, see xlsxSheetReader.formula27+ hasF bool // the cell has a formula element, even an empty one2628 }2729 2830 // kept reports whether excelize's row reader would return the cell: it@@ -47,6 +49,8 @@ type xlsxSheetReader struct {
4749 defaultWidth float644850 baseWidth int4951 52+ colStyles []int // by column, see colStyle53+5054 inData bool // inside <sheetData>5155 row xlsxRowData5256 shared map[int]sharedFormula@@ -120,8 +124,22 @@ func (r *xlsxSheetReader) readHead() error {
120124 }121125 122126 // colStyle is the cell format of column col (from 1): the first <col>123-// covering it with a style.127+// covering it with a style. Columns up to sheet.MaxCols, the ones asked128+// for once per cell, are looked up once.124129 func (r *xlsxSheetReader) colStyle(col int) int {130+ if r.colStyles == nil {131+ r.colStyles = make([]int, sheet.MaxCols)132+ for _, c := range r.cols {133+ for i := max(c.min, 1); i <= min(c.max, sheet.MaxCols); i++ {134+ if r.colStyles[i-1] == 0 {135+ r.colStyles[i-1] = c.style136+ }137+ }138+ }139+ }140+ if col >= 1 && col <= len(r.colStyles) {141+ return r.colStyles[col-1]142+ }125143 for _, c := range r.cols {126144 if c.min <= col && col <= c.max && c.style != 0 {127145 return c.style@@ -258,7 +276,10 @@ func (r *xlsxSheetReader) readCell(se xml.StartElement, prev int) (int, error) {
258276 }259277 c.value = r.value(c.typ, v, is, hasIS)260278 if c.hasF {261- c.formula = r.formula(f, c.col, r.row.num)279+ c.f = f280+ if _, ok := r.shared[f.si]; !ok && f.typ == "shared" && f.si >= 0 && f.ref != "" {281+ r.shared[f.si] = sharedFormula{text: f.text, col: c.col, row: r.row.num}282+ }262283 }263284 r.row.cells = append(r.row.cells, c)264285 return c.col, nil@@ -302,22 +323,26 @@ func (r *xlsxSheetReader) readFormula(se xml.StartElement) (cellFormula, error)
302323 return f, err303324 }304325 305-// formula is the formula of the cell at col, row: its own text, or for306-// a cell sharing a formula, the master's with its relative references307-// moved by the distance between the cells.308-func (r *xlsxSheetReader) formula(f cellFormula, col, row int) string {309- if f.typ != "shared" || f.si < 0 {310- return f.text326+// formula is the formula of cell c of the current row: its own text,327+// or for a cell sharing a formula, the first cell's with its relative328+// references moved by the distance between the cells ("" when the first329+// is missing). Shared formulas are expanded only on request, and only330+// up to the limit on their text: a small file of shared formulas could331+// otherwise expand to gigabytes.332+func (r *xlsxSheetReader) formula(c *xlsxCell) (string, error) {333+ f := c.f334+ if !c.hasF || f.typ != "shared" || f.si < 0 {335+ return f.text, nil311336 }312337 m, ok := r.shared[f.si]313338 if !ok {314- if f.ref == "" {315- return "" // the master is missing316- }317- m = sharedFormula{text: f.text, col: col, row: row}318- r.shared[f.si] = m339+ return "", nil340+ }341+ out := shiftFormula(m.text, c.col-m.col, r.row.num-m.row)342+ if r.bk.expanded += int64(len(out)); r.bk.expanded > r.bk.pkg.lim.shared {343+ return "", fmt.Errorf("shared formulas expand to more than %s: %w", mb(r.bk.pkg.lim.shared), errXLSXLimit)319344 }320- return shiftFormula(m.text, col-m.col, row-m.row)345+ return out, nil321346 }322347 323348 // parseCellRef parses an A1 reference (without $) into a column and row