[{"data":1,"prerenderedAt":2213},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fread-untrusted-excel-files-safely-in-python":3,"surround:\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fread-untrusted-excel-files-safely-in-python":2178,"ancestors:\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fread-untrusted-excel-files-safely-in-python":2187,"children:\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fread-untrusted-excel-files-safely-in-python":2192,"related:\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fread-untrusted-excel-files-safely-in-python":2193},{"id":4,"title":5,"body":6,"dateModified":2145,"datePublished":2145,"description":2146,"extension":2147,"faq":2148,"keyword":2157,"kind":2158,"meta":2159,"navigation":321,"path":2167,"seo":2168,"slug":2171,"stem":2172,"topics":2173,"type":2176,"__hash__":2177},"docs\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fread-untrusted-excel-files-safely-in-python\u002Findex.md","Read Untrusted Excel Files Safely in Python",{"type":7,"value":8,"toc":2133},"minimark",[9,24,29,60,66,257,261,293,618,636,640,647,1102,1110,1114,1120,1251,1264,1345,1349,1367,1444,1447,1551,1555,1562,1870,1886,2010,2013,2017,2055,2059,2065,2069,2075,2081,2087,2093,2097,2129],[10,11,12,13,17,18,23],"p",{},"An upload form that accepts spreadsheets, an inbox that processes attachments, a partner who sends files by SFTP: each means Python code opening files someone else made. An ",[14,15,16],"code",{},".xlsx"," file is a zip archive of XML documents, and both layers can be abused — a few kilobytes that expand to gigabytes, XML that expands itself, formula results that lie, macros waiting for someone to open the file in Excel. Reading data with pandas or openpyxl does not run macros, but it does parse whatever it is given. This page builds a set of checks to run before and while parsing an untrusted workbook. It is part of ",[19,20,22],"a",{"href":21},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002F","Error Handling and Logging in Excel Automation",".",[25,26,28],"h2",{"id":27},"prerequisites","Prerequisites",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl defusedxml\n","bash","",[14,37,38],{"__ignoreMap":35},[39,40,43,47,51,54,57],"span",{"class":41,"line":42},"line",1,[39,44,46],{"class":45},"sMTad","pip",[39,48,50],{"class":49},"srMev"," install",[39,52,53],{"class":49}," pandas",[39,55,56],{"class":49}," openpyxl",[39,58,59],{"class":49}," defusedxml\n",[10,61,62,65],{},[14,63,64],{},"uploads\u002F"," holds five files as a real inbox might: a normal order, an \"Excel export\" from a web portal that is really HTML, a decompression bomb, a workbook whose XML defines self-expanding entities, and an invoice whose formula result has been tampered with. The checks below are ordered from cheapest to most expensive; a file that fails one is rejected before the next runs.",[67,68,76,77,76,81,76,85,76,93,76,103,76,114,76,121,76,125,76,130,76,136,76,141,76,145,76,152,76,158,76,162,76,166,76,170,76,174,76,177,76,180,76,186,76,190,76,193,76,197,76,200,76,205,76,212,76,217,76,220,76,223,76,226,76,230,76,233,76,238,76,245,76,250,76,254],"svg",{"viewBox":69,"role":70,"ariaLabelledBy":71,"xmlns":74,"style":75},"4 0 752 160","img",[72,73],"ut-gates-t","ut-gates-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[78,79,80],"title",{"id":72},"Checks before and during parsing",[82,83,84],"desc",{"id":73},"A pipeline of checks. First the file signature and size, then the zip contents without extracting, then parsing with defusedxml in a separate process with memory and time limits, then validating the data, including recomputing formula results rather than trusting cached ones.",[86,87],"rect",{"x":88,"y":89,"width":90,"height":91,"fill":92},"4","0","752","160","#ffffff",[86,94],{"x":95,"y":96,"width":97,"height":98,"rx":99,"fill":100,"stroke":101,"style":102},"20.0","16.0","115.2","128.0","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[104,105,107],"g",{"transform":106},"translate(65.6,37.8) scale(1.000)",[108,109],"path",{"d":110,"fill":111,"stroke":112,"style":113},"M6 3h9l4 4v14H6z M15 3v4h4","none","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[115,116,120],"text",{"x":117,"y":118,"style":119},"77.6","84.2","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Signature",[115,122,124],{"x":117,"y":123,"style":119},"101.8","and size",[115,126,129],{"x":117,"y":127,"style":128},"121.2","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","what is it really?",[41,131],{"x1":132,"y1":133,"x2":134,"y2":133,"stroke":135,"style":102},"140.2","80.0","162.2","var(--brand,#5b5cf0)",[137,138],"polygon",{"points":139,"fill":140},"166.2,80.0 157.2,85.0 157.2,75.0","#5b5cf0",[86,142],{"x":143,"y":96,"width":97,"height":98,"rx":99,"fill":144,"stroke":135,"style":102},"171.2","#f0f4ff",[104,146,148],{"transform":147},"translate(216.8,29.0) scale(1.000)",[108,149],{"d":150,"fill":111,"stroke":151,"style":113},"M12 2.5l8.5 4.7v9.6L12 21.5l-8.5-4.7V7.2z M3.5 7.2l8.5 4.7 8.5-4.7 M12 11.9v9.6","var(--brand-strong,#4338ca)",[115,153,157],{"x":154,"y":155,"style":156},"228.8","75.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Zip",[115,159,161],{"x":154,"y":160,"style":156},"93.0","inspection",[115,163,165],{"x":154,"y":164,"style":128},"112.5","no extraction",[115,167,169],{"x":154,"y":168,"style":128},"130.0","yet",[41,171],{"x1":172,"y1":133,"x2":173,"y2":133,"stroke":135,"style":102},"291.4","313.4",[137,175],{"points":176,"fill":140},"317.4,80.0 308.4,85.0 308.4,75.0",[86,178],{"x":179,"y":96,"width":97,"height":98,"rx":99,"fill":144,"stroke":135,"style":102},"322.4",[104,181,183],{"transform":182},"translate(368.0,46.5) scale(1.000)",[108,184],{"d":185,"fill":111,"stroke":151,"style":113},"M6 11h12v9H6z M8.5 11V8a3.5 3.5 0 0 1 7 0v3",[115,187,189],{"x":188,"y":160,"style":156},"380.0","Safe parser",[115,191,192],{"x":188,"y":164,"style":128},"defusedxml",[41,194],{"x1":195,"y1":133,"x2":196,"y2":133,"stroke":135,"style":102},"442.6","464.6",[137,198],{"points":199,"fill":140},"468.6,80.0 459.6,85.0 459.6,75.0",[86,201],{"x":202,"y":96,"width":97,"height":98,"rx":99,"fill":203,"stroke":204,"style":102},"473.6","#d9f4f1","var(--teal,#0f9488)",[104,206,208],{"transform":207},"translate(519.2,29.0) scale(1.000)",[108,209],{"d":210,"fill":111,"stroke":211,"style":113},"M4 4h16v6H4z M4 14h16v6H4z M8 7h.01 M8 17h.01","var(--teal-ink,#0b6157)",[115,213,216],{"x":214,"y":155,"style":215},"531.2","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Sandboxed",[115,218,219],{"x":214,"y":160,"style":215},"process",[115,221,222],{"x":214,"y":164,"style":128},"memory and",[115,224,225],{"x":214,"y":168,"style":128},"time limits",[41,227],{"x1":228,"y1":133,"x2":229,"y2":133,"stroke":135,"style":102},"593.8","615.8",[137,231],{"points":232,"fill":140},"619.8,80.0 610.8,85.0 610.8,75.0",[86,234],{"x":235,"y":96,"width":97,"height":98,"rx":99,"fill":236,"stroke":237,"style":102},"624.8","#fdefd8","var(--gold,#b4740a)",[104,239,241],{"transform":240},"translate(670.4,37.8) scale(1.000)",[108,242],{"d":243,"fill":111,"stroke":244,"style":113},"M5 12.5l4.5 4.5L19 7","var(--gold-ink,#7a4e06)",[115,246,249],{"x":247,"y":118,"style":248},"682.4","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Validate data",[115,251,253],{"x":247,"y":252,"style":128},"103.8","recompute",[115,255,256],{"x":247,"y":127,"style":128},"formulas",[25,258,260],{"id":259},"step-1-check-what-the-file-really-is","Step 1 — Check what the file really is",[10,262,263,264,266,267,270,271,274,275,278,279,282,283,286,287,289,290,292],{},"Extensions are chosen by whoever named the file. The first bytes say what it is: ",[14,265,16],{},", ",[14,268,269],{},".xlsm"," and ",[14,272,273],{},".xlsb"," files are zip archives starting with ",[14,276,277],{},"PK\\x03\\x04","; legacy ",[14,280,281],{},".xls"," files are OLE compound documents starting with ",[14,284,285],{},"D0 CF 11 E0",". Web portals often serve HTML tables with an ",[14,288,281],{}," or ",[14,291,16],{}," name. Check the signature and the size before anything else:",[30,294,298],{"className":295,"code":296,"language":297,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","from pathlib import Path\n\nMAX_UPLOAD = 20 * 1024 * 1024\nSIGNATURES = {b\"PK\\x03\\x04\": \"zip (xlsx\u002Fxlsm\u002Fxlsb)\", b\"\\xd0\\xcf\\x11\\xe0\": \"OLE (xls)\"}\n\ndef sniff(path):\n    size = path.stat().st_size\n    if size > MAX_UPLOAD:\n        return f\"rejected: {size:,} bytes is over the upload limit\"\n    head = path.read_bytes()[:8]\n    for magic, kind in SIGNATURES.items():\n        if head.startswith(magic):\n            return kind\n    return f\"rejected: not a spreadsheet (starts with {head[:6]!r})\"\n\nfor f in sorted(Path(\"uploads\").iterdir()):\n    print(f\"{f.name:26} {sniff(f)}\")\n","python",[14,299,300,316,323,347,396,401,414,426,444,471,488,506,515,524,554,559,582],{"__ignoreMap":35},[39,301,302,306,310,313],{"class":41,"line":42},[39,303,305],{"class":304},"s-kum","from",[39,307,309],{"class":308},"skGVy"," pathlib ",[39,311,312],{"class":304},"import",[39,314,315],{"class":308}," Path\n",[39,317,319],{"class":41,"line":318},2,[39,320,322],{"emptyLinePlaceholder":321},true,"\n",[39,324,326,330,333,336,339,342,344],{"class":41,"line":325},3,[39,327,329],{"class":328},"sP0c6","MAX_UPLOAD",[39,331,332],{"class":304}," =",[39,334,335],{"class":328}," 20",[39,337,338],{"class":304}," *",[39,340,341],{"class":328}," 1024",[39,343,338],{"class":304},[39,345,346],{"class":328}," 1024\n",[39,348,350,353,355,358,361,364,368,371,374,377,379,381,383,386,388,390,393],{"class":41,"line":349},4,[39,351,352],{"class":328},"SIGNATURES",[39,354,332],{"class":304},[39,356,357],{"class":308}," {",[39,359,360],{"class":304},"b",[39,362,363],{"class":49},"\"PK",[39,365,367],{"class":366},"sSjpA","\\x03\\x04",[39,369,370],{"class":49},"\"",[39,372,373],{"class":308},": ",[39,375,376],{"class":49},"\"zip (xlsx\u002Fxlsm\u002Fxlsb)\"",[39,378,266],{"class":308},[39,380,360],{"class":304},[39,382,370],{"class":49},[39,384,385],{"class":366},"\\xd0\\xcf\\x11\\xe0",[39,387,370],{"class":49},[39,389,373],{"class":308},[39,391,392],{"class":49},"\"OLE (xls)\"",[39,394,395],{"class":308},"}\n",[39,397,399],{"class":41,"line":398},5,[39,400,322],{"emptyLinePlaceholder":321},[39,402,404,407,411],{"class":41,"line":403},6,[39,405,406],{"class":304},"def",[39,408,410],{"class":409},"s_Opv"," sniff",[39,412,413],{"class":308},"(path):\n",[39,415,417,420,423],{"class":41,"line":416},7,[39,418,419],{"class":308},"    size ",[39,421,422],{"class":304},"=",[39,424,425],{"class":308}," path.stat().st_size\n",[39,427,429,432,435,438,441],{"class":41,"line":428},8,[39,430,431],{"class":304},"    if",[39,433,434],{"class":308}," size ",[39,436,437],{"class":304},">",[39,439,440],{"class":328}," MAX_UPLOAD",[39,442,443],{"class":308},":\n",[39,445,447,450,453,456,459,462,465,468],{"class":41,"line":446},9,[39,448,449],{"class":304},"        return",[39,451,452],{"class":304}," f",[39,454,455],{"class":49},"\"rejected: ",[39,457,458],{"class":366},"{",[39,460,461],{"class":308},"size",[39,463,464],{"class":304},":,",[39,466,467],{"class":366},"}",[39,469,470],{"class":49}," bytes is over the upload limit\"\n",[39,472,474,477,479,482,485],{"class":41,"line":473},10,[39,475,476],{"class":308},"    head ",[39,478,422],{"class":304},[39,480,481],{"class":308}," path.read_bytes()[:",[39,483,484],{"class":328},"8",[39,486,487],{"class":308},"]\n",[39,489,491,494,497,500,503],{"class":41,"line":490},11,[39,492,493],{"class":304},"    for",[39,495,496],{"class":308}," magic, kind ",[39,498,499],{"class":304},"in",[39,501,502],{"class":328}," SIGNATURES",[39,504,505],{"class":308},".items():\n",[39,507,509,512],{"class":41,"line":508},12,[39,510,511],{"class":304},"        if",[39,513,514],{"class":308}," head.startswith(magic):\n",[39,516,518,521],{"class":41,"line":517},13,[39,519,520],{"class":304},"            return",[39,522,523],{"class":308}," kind\n",[39,525,527,530,532,535,537,540,543,546,549,551],{"class":41,"line":526},14,[39,528,529],{"class":304},"    return",[39,531,452],{"class":304},[39,533,534],{"class":49},"\"rejected: not a spreadsheet (starts with ",[39,536,458],{"class":366},[39,538,539],{"class":308},"head[:",[39,541,542],{"class":328},"6",[39,544,545],{"class":308},"]",[39,547,548],{"class":304},"!r",[39,550,467],{"class":366},[39,552,553],{"class":49},")\"\n",[39,555,557],{"class":41,"line":556},15,[39,558,322],{"emptyLinePlaceholder":321},[39,560,562,565,568,570,573,576,579],{"class":41,"line":561},16,[39,563,564],{"class":304},"for",[39,566,567],{"class":308}," f ",[39,569,499],{"class":304},[39,571,572],{"class":328}," sorted",[39,574,575],{"class":308},"(Path(",[39,577,578],{"class":49},"\"uploads\"",[39,580,581],{"class":308},").iterdir()):\n",[39,583,585,588,591,594,596,598,601,604,606,608,611,613,615],{"class":41,"line":584},17,[39,586,587],{"class":328},"    print",[39,589,590],{"class":308},"(",[39,592,593],{"class":304},"f",[39,595,370],{"class":49},[39,597,458],{"class":366},[39,599,600],{"class":308},"f.name",[39,602,603],{"class":304},":26",[39,605,467],{"class":366},[39,607,357],{"class":366},[39,609,610],{"class":308},"sniff(f)",[39,612,467],{"class":366},[39,614,370],{"class":49},[39,616,617],{"class":308},")\n",[10,619,620,621,624,625,628,629,633,634,23],{},"The portal \"export\" starts with ",[14,622,623],{},"\u003Chtml>",". pandas can read HTML tables with ",[14,626,627],{},"read_html",", so such files can be handled on purpose, but they should never reach an Excel parser by accident. ",[19,630,632],{"href":631},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-badzipfile-error-when-reading-excel-in-python\u002F","Fix BadZipFile When Reading an Excel File in Python"," covers the other formats that masquerade as ",[14,635,16],{},[25,637,639],{"id":638},"step-2-inspect-the-zip-before-extracting-anything","Step 2 — Inspect the zip before extracting anything",[10,641,642,643,646],{},"A decompression bomb is small on disk and huge in memory: ",[14,644,645],{},"bomb.xlsx"," passed the size check but one of its parts expands to tens of megabytes of whitespace, and crafted files go far beyond that. The zip directory records every part's uncompressed size, so it can be checked without decompressing anything:",[30,648,650],{"className":295,"code":649,"language":297,"meta":35,"style":35},"import zipfile\n\nMAX_TOTAL = 50 * 1024 * 1024       # uncompressed bytes across all parts\nMAX_RATIO = 100                    # uncompressed \u002F compressed\n\ndef inspect_zip(path):\n    with zipfile.ZipFile(path) as z:\n        infos = z.infolist()\n        total = sum(i.file_size for i in infos)\n        packed = sum(i.compress_size for i in infos) or 1\n        worst = max(infos, key=lambda i: i.file_size \u002F max(i.compress_size, 1))\n        names = [i.filename for i in infos]\n    problems = []\n    if total > MAX_TOTAL:\n        problems.append(f\"expands to {total:,} bytes\")\n    if total \u002F packed > MAX_RATIO:\n        problems.append(f\"compression ratio {total \u002F packed:,.0f}:1 ({worst.filename})\")\n    if any(n.startswith(\"xl\u002FvbaProject\") for n in names):\n        problems.append(\"contains macros\")\n    if any(n.startswith(\"xl\u002FexternalLinks\u002F\") for n in names):\n        problems.append(\"links to external workbooks\")\n    return problems\n\nfor f in sorted(Path(\"uploads\").glob(\"*.xlsx\")):\n    if sniff(f).startswith(\"zip\"):\n        print(f\"{f.name:26} {inspect_zip(f) or 'ok'}\")\n",[14,651,652,659,663,685,698,702,711,725,735,758,785,822,841,851,865,889,907,946,973,983,1005,1015,1023,1028,1052,1066],{"__ignoreMap":35},[39,653,654,656],{"class":41,"line":42},[39,655,312],{"class":304},[39,657,658],{"class":308}," zipfile\n",[39,660,661],{"class":41,"line":318},[39,662,322],{"emptyLinePlaceholder":321},[39,664,665,668,670,673,675,677,679,681],{"class":41,"line":325},[39,666,667],{"class":328},"MAX_TOTAL",[39,669,332],{"class":304},[39,671,672],{"class":328}," 50",[39,674,338],{"class":304},[39,676,341],{"class":328},[39,678,338],{"class":304},[39,680,341],{"class":328},[39,682,684],{"class":683},"s-wDw","       # uncompressed bytes across all parts\n",[39,686,687,690,692,695],{"class":41,"line":349},[39,688,689],{"class":328},"MAX_RATIO",[39,691,332],{"class":304},[39,693,694],{"class":328}," 100",[39,696,697],{"class":683},"                    # uncompressed \u002F compressed\n",[39,699,700],{"class":41,"line":398},[39,701,322],{"emptyLinePlaceholder":321},[39,703,704,706,709],{"class":41,"line":403},[39,705,406],{"class":304},[39,707,708],{"class":409}," inspect_zip",[39,710,413],{"class":308},[39,712,713,716,719,722],{"class":41,"line":416},[39,714,715],{"class":304},"    with",[39,717,718],{"class":308}," zipfile.ZipFile(path) ",[39,720,721],{"class":304},"as",[39,723,724],{"class":308}," z:\n",[39,726,727,730,732],{"class":41,"line":428},[39,728,729],{"class":308},"        infos ",[39,731,422],{"class":304},[39,733,734],{"class":308}," z.infolist()\n",[39,736,737,740,742,745,748,750,753,755],{"class":41,"line":446},[39,738,739],{"class":308},"        total ",[39,741,422],{"class":304},[39,743,744],{"class":328}," sum",[39,746,747],{"class":308},"(i.file_size ",[39,749,564],{"class":304},[39,751,752],{"class":308}," i ",[39,754,499],{"class":304},[39,756,757],{"class":308}," infos)\n",[39,759,760,763,765,767,770,772,774,776,779,782],{"class":41,"line":473},[39,761,762],{"class":308},"        packed ",[39,764,422],{"class":304},[39,766,744],{"class":328},[39,768,769],{"class":308},"(i.compress_size ",[39,771,564],{"class":304},[39,773,752],{"class":308},[39,775,499],{"class":304},[39,777,778],{"class":308}," infos) ",[39,780,781],{"class":304},"or",[39,783,784],{"class":328}," 1\n",[39,786,787,790,792,795,798,802,805,808,811,813,816,819],{"class":41,"line":490},[39,788,789],{"class":308},"        worst ",[39,791,422],{"class":304},[39,793,794],{"class":328}," max",[39,796,797],{"class":308},"(infos, ",[39,799,801],{"class":800},"sa561","key",[39,803,804],{"class":304},"=lambda",[39,806,807],{"class":308}," i: i.file_size ",[39,809,810],{"class":304},"\u002F",[39,812,794],{"class":328},[39,814,815],{"class":308},"(i.compress_size, ",[39,817,818],{"class":328},"1",[39,820,821],{"class":308},"))\n",[39,823,824,827,829,832,834,836,838],{"class":41,"line":508},[39,825,826],{"class":308},"        names ",[39,828,422],{"class":304},[39,830,831],{"class":308}," [i.filename ",[39,833,564],{"class":304},[39,835,752],{"class":308},[39,837,499],{"class":304},[39,839,840],{"class":308}," infos]\n",[39,842,843,846,848],{"class":41,"line":517},[39,844,845],{"class":308},"    problems ",[39,847,422],{"class":304},[39,849,850],{"class":308}," []\n",[39,852,853,855,858,860,863],{"class":41,"line":526},[39,854,431],{"class":304},[39,856,857],{"class":308}," total ",[39,859,437],{"class":304},[39,861,862],{"class":328}," MAX_TOTAL",[39,864,443],{"class":308},[39,866,867,870,872,875,877,880,882,884,887],{"class":41,"line":556},[39,868,869],{"class":308},"        problems.append(",[39,871,593],{"class":304},[39,873,874],{"class":49},"\"expands to ",[39,876,458],{"class":366},[39,878,879],{"class":308},"total",[39,881,464],{"class":304},[39,883,467],{"class":366},[39,885,886],{"class":49}," bytes\"",[39,888,617],{"class":308},[39,890,891,893,895,897,900,902,905],{"class":41,"line":561},[39,892,431],{"class":304},[39,894,857],{"class":308},[39,896,810],{"class":304},[39,898,899],{"class":308}," packed ",[39,901,437],{"class":304},[39,903,904],{"class":328}," MAX_RATIO",[39,906,443],{"class":308},[39,908,909,911,913,916,918,921,923,926,929,931,934,936,939,941,944],{"class":41,"line":584},[39,910,869],{"class":308},[39,912,593],{"class":304},[39,914,915],{"class":49},"\"compression ratio ",[39,917,458],{"class":366},[39,919,920],{"class":308},"total ",[39,922,810],{"class":304},[39,924,925],{"class":308}," packed",[39,927,928],{"class":304},":,.0f",[39,930,467],{"class":366},[39,932,933],{"class":49},":1 (",[39,935,458],{"class":366},[39,937,938],{"class":308},"worst.filename",[39,940,467],{"class":366},[39,942,943],{"class":49},")\"",[39,945,617],{"class":308},[39,947,949,951,954,957,960,963,965,968,970],{"class":41,"line":948},18,[39,950,431],{"class":304},[39,952,953],{"class":328}," any",[39,955,956],{"class":308},"(n.startswith(",[39,958,959],{"class":49},"\"xl\u002FvbaProject\"",[39,961,962],{"class":308},") ",[39,964,564],{"class":304},[39,966,967],{"class":308}," n ",[39,969,499],{"class":304},[39,971,972],{"class":308}," names):\n",[39,974,976,978,981],{"class":41,"line":975},19,[39,977,869],{"class":308},[39,979,980],{"class":49},"\"contains macros\"",[39,982,617],{"class":308},[39,984,986,988,990,992,995,997,999,1001,1003],{"class":41,"line":985},20,[39,987,431],{"class":304},[39,989,953],{"class":328},[39,991,956],{"class":308},[39,993,994],{"class":49},"\"xl\u002FexternalLinks\u002F\"",[39,996,962],{"class":308},[39,998,564],{"class":304},[39,1000,967],{"class":308},[39,1002,499],{"class":304},[39,1004,972],{"class":308},[39,1006,1008,1010,1013],{"class":41,"line":1007},21,[39,1009,869],{"class":308},[39,1011,1012],{"class":49},"\"links to external workbooks\"",[39,1014,617],{"class":308},[39,1016,1018,1020],{"class":41,"line":1017},22,[39,1019,529],{"class":304},[39,1021,1022],{"class":308}," problems\n",[39,1024,1026],{"class":41,"line":1025},23,[39,1027,322],{"emptyLinePlaceholder":321},[39,1029,1031,1033,1035,1037,1039,1041,1043,1046,1049],{"class":41,"line":1030},24,[39,1032,564],{"class":304},[39,1034,567],{"class":308},[39,1036,499],{"class":304},[39,1038,572],{"class":328},[39,1040,575],{"class":308},[39,1042,578],{"class":49},[39,1044,1045],{"class":308},").glob(",[39,1047,1048],{"class":49},"\"*.xlsx\"",[39,1050,1051],{"class":308},")):\n",[39,1053,1055,1057,1060,1063],{"class":41,"line":1054},25,[39,1056,431],{"class":304},[39,1058,1059],{"class":308}," sniff(f).startswith(",[39,1061,1062],{"class":49},"\"zip\"",[39,1064,1065],{"class":308},"):\n",[39,1067,1069,1072,1074,1076,1078,1080,1082,1084,1086,1088,1091,1093,1096,1098,1100],{"class":41,"line":1068},26,[39,1070,1071],{"class":328},"        print",[39,1073,590],{"class":308},[39,1075,593],{"class":304},[39,1077,370],{"class":49},[39,1079,458],{"class":366},[39,1081,600],{"class":308},[39,1083,603],{"class":304},[39,1085,467],{"class":366},[39,1087,357],{"class":366},[39,1089,1090],{"class":308},"inspect_zip(f) ",[39,1092,781],{"class":304},[39,1094,1095],{"class":49}," 'ok'",[39,1097,467],{"class":366},[39,1099,370],{"class":49},[39,1101,617],{"class":308},[10,1103,1104,1105,1109],{},"Tune the limits to your real files: a normal workbook compresses about 5 to 20 times; ratios in the hundreds or thousands are a strong signal. Macros are not a threat to pandas, which never runs them, but a file with macros that is later opened in Excel by a colleague is; flag it, and see ",[19,1106,1108],{"href":1107},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fextract-vba-macro-code-from-an-excel-file\u002F","Extract VBA Macro Code from an Excel File"," to review it without opening Excel.",[25,1111,1113],{"id":1112},"step-3-make-sure-the-xml-parser-is-hardened","Step 3 — Make sure the XML parser is hardened",[10,1115,1116,1117,1119],{},"XML can define entities that expand into other entities; nested a few levels deep, a few hundred bytes become gigabytes (the \"billion laughs\" attack). openpyxl uses the ",[14,1118,192],{}," package when it is installed, which refuses entity definitions. It is an optional dependency, so check it is present in the environment that processes uploads:",[30,1121,1123],{"className":295,"code":1122,"language":297,"meta":35,"style":35},"import openpyxl\n\nprint(\"openpyxl uses defusedxml:\", openpyxl.DEFUSEDXML)\ntry:\n    pd.read_excel(\"uploads\u002Fentities.xlsx\", engine=\"openpyxl\")\nexcept ValueError as e:\n    print(\"refused:\", str(e).splitlines()[0])\n    print(\"cause:\", type(e.__cause__).__name__ if e.__cause__ else None)\n",[14,1124,1125,1132,1136,1154,1161,1181,1195,1217],{"__ignoreMap":35},[39,1126,1127,1129],{"class":41,"line":42},[39,1128,312],{"class":304},[39,1130,1131],{"class":308}," openpyxl\n",[39,1133,1134],{"class":41,"line":318},[39,1135,322],{"emptyLinePlaceholder":321},[39,1137,1138,1141,1143,1146,1149,1152],{"class":41,"line":325},[39,1139,1140],{"class":328},"print",[39,1142,590],{"class":308},[39,1144,1145],{"class":49},"\"openpyxl uses defusedxml:\"",[39,1147,1148],{"class":308},", openpyxl.",[39,1150,1151],{"class":328},"DEFUSEDXML",[39,1153,617],{"class":308},[39,1155,1156,1159],{"class":41,"line":349},[39,1157,1158],{"class":304},"try",[39,1160,443],{"class":308},[39,1162,1163,1166,1169,1171,1174,1176,1179],{"class":41,"line":398},[39,1164,1165],{"class":308},"    pd.read_excel(",[39,1167,1168],{"class":49},"\"uploads\u002Fentities.xlsx\"",[39,1170,266],{"class":308},[39,1172,1173],{"class":800},"engine",[39,1175,422],{"class":304},[39,1177,1178],{"class":49},"\"openpyxl\"",[39,1180,617],{"class":308},[39,1182,1183,1186,1189,1192],{"class":41,"line":403},[39,1184,1185],{"class":304},"except",[39,1187,1188],{"class":328}," ValueError",[39,1190,1191],{"class":304}," as",[39,1193,1194],{"class":308}," e:\n",[39,1196,1197,1199,1201,1204,1206,1209,1212,1214],{"class":41,"line":416},[39,1198,587],{"class":328},[39,1200,590],{"class":308},[39,1202,1203],{"class":49},"\"refused:\"",[39,1205,266],{"class":308},[39,1207,1208],{"class":328},"str",[39,1210,1211],{"class":308},"(e).splitlines()[",[39,1213,89],{"class":328},[39,1215,1216],{"class":308},"])\n",[39,1218,1219,1221,1223,1226,1228,1231,1234,1237,1240,1243,1246,1249],{"class":41,"line":428},[39,1220,587],{"class":328},[39,1222,590],{"class":308},[39,1224,1225],{"class":49},"\"cause:\"",[39,1227,266],{"class":308},[39,1229,1230],{"class":328},"type",[39,1232,1233],{"class":308},"(e.__cause__).",[39,1235,1236],{"class":328},"__name__",[39,1238,1239],{"class":304}," if",[39,1241,1242],{"class":308}," e.__cause__ ",[39,1244,1245],{"class":304},"else",[39,1247,1248],{"class":328}," None",[39,1250,617],{"class":308},[10,1252,1253,1254,1256,1257,1259,1260,1263],{},"With ",[14,1255,192],{}," installed, openpyxl refuses the file. Without it, the same file is read and the entities are expanded: the harmless three-level example here turns into a 600-character cell, and a hostile one would exhaust memory. Add ",[14,1258,192],{}," to the project's requirements, and assert ",[14,1261,1262],{},"openpyxl.DEFUSEDXML"," at start-up so a missing package fails loudly instead of silently lowering the protection.",[67,1265,76,1270,76,1273,76,1276,76,1279,76,1284,76,1290,76,1296,76,1302,76,1307,76,1311,76,1315,76,1320,76,1327,76,1332,76,1336,76,1339,76,1342],{"viewBox":1266,"role":70,"ariaLabelledBy":1267,"xmlns":74,"style":75},"4 0 752 207",[1268,1269],"ut-xml-t","ut-xml-d",[78,1271,1272],{"id":1268},"The same file with and without defusedxml",[82,1274,1275],{"id":1269},"Two panels. With defusedxml installed, openpyxl refuses a workbook that defines XML entities and pandas raises an error. Without it, the entities are expanded while parsing, which a hostile file uses to exhaust memory.",[86,1277],{"x":88,"y":89,"width":90,"height":1278,"fill":92},"207",[86,1280],{"x":95,"y":96,"width":1281,"height":1282,"rx":1283,"fill":203,"stroke":204,"style":102},"351.0","175.0","14",[104,1285,1287],{"transform":1286},"translate(182.5,29.0) scale(1.083)",[108,1288],{"d":185,"fill":111,"stroke":211,"style":1289},"stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[115,1291,1295],{"x":1292,"y":1293,"style":1294},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","defusedxml installed",[41,1297],{"x1":1298,"y1":1299,"x2":1300,"y2":1299,"stroke":204,"style":1301},"36.0","91.0","355.0","stroke-width:1px",[115,1303,1306],{"x":1292,"y":1304,"style":1305},"111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","entity definitions refused",[115,1308,1310],{"x":1292,"y":1309,"style":1305},"137.0","ValueError, file rejected",[115,1312,1314],{"x":1292,"y":1313,"style":1305},"163.0","openpyxl.DEFUSEDXML is True",[86,1316],{"x":1317,"y":96,"width":1281,"height":1282,"rx":1283,"fill":1318,"stroke":1319,"style":102},"389.0","#fce9e9","var(--danger,#dc2626)",[104,1321,1323],{"transform":1322},"translate(551.5,29.0) scale(1.083)",[108,1324],{"d":1325,"fill":111,"stroke":1326,"style":1289},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","var(--accent-ink,#be185d)",[115,1328,1331],{"x":1329,"y":1293,"style":1330},"564.5","font-size:14px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","Not installed",[41,1333],{"x1":1334,"y1":1299,"x2":1335,"y2":1299,"stroke":1319,"style":1301},"405.0","724.0",[115,1337,1338],{"x":1329,"y":1304,"style":1305},"entities expanded",[115,1340,1341],{"x":1329,"y":1309,"style":1305},"memory grows with nesting",[115,1343,1344],{"x":1329,"y":1313,"style":1305},"no warning",[25,1346,1348],{"id":1347},"step-4-dont-trust-cached-formula-results","Step 4 — Don't trust cached formula results",[10,1350,1351,1352,1354,1355,1358,1359,1362,1363,1366],{},"An ",[14,1353,16],{}," file stores each formula together with its last calculated result, and pandas, like openpyxl with ",[14,1356,1357],{},"data_only=True",", reads the stored result. Nothing guarantees that result matches the formula. ",[14,1360,1361],{},"invoice.xlsx"," says ",[14,1364,1365],{},"Total = B2*C2",", with a quantity of 3 and a price of 2.50, and a stored result of 7,500:",[30,1368,1370],{"className":295,"code":1369,"language":297,"meta":35,"style":35},"from openpyxl import load_workbook\n\nprint(pd.read_excel(\"uploads\u002Finvoice.xlsx\").to_string(index=False))\nws = load_workbook(\"uploads\u002Finvoice.xlsx\").active\nprint(\"formula in D2:\", ws[\"D2\"].value)\n",[14,1371,1372,1384,1388,1411,1426],{"__ignoreMap":35},[39,1373,1374,1376,1379,1381],{"class":41,"line":42},[39,1375,305],{"class":304},[39,1377,1378],{"class":308}," openpyxl ",[39,1380,312],{"class":304},[39,1382,1383],{"class":308}," load_workbook\n",[39,1385,1386],{"class":41,"line":318},[39,1387,322],{"emptyLinePlaceholder":321},[39,1389,1390,1392,1395,1398,1401,1404,1406,1409],{"class":41,"line":325},[39,1391,1140],{"class":328},[39,1393,1394],{"class":308},"(pd.read_excel(",[39,1396,1397],{"class":49},"\"uploads\u002Finvoice.xlsx\"",[39,1399,1400],{"class":308},").to_string(",[39,1402,1403],{"class":800},"index",[39,1405,422],{"class":304},[39,1407,1408],{"class":328},"False",[39,1410,821],{"class":308},[39,1412,1413,1416,1418,1421,1423],{"class":41,"line":349},[39,1414,1415],{"class":308},"ws ",[39,1417,422],{"class":304},[39,1419,1420],{"class":308}," load_workbook(",[39,1422,1397],{"class":49},[39,1424,1425],{"class":308},").active\n",[39,1427,1428,1430,1432,1435,1438,1441],{"class":41,"line":398},[39,1429,1140],{"class":328},[39,1431,590],{"class":308},[39,1433,1434],{"class":49},"\"formula in D2:\"",[39,1436,1437],{"class":308},", ws[",[39,1439,1440],{"class":49},"\"D2\"",[39,1442,1443],{"class":308},"].value)\n",[10,1445,1446],{},"For any figure that matters, read the inputs and recompute in Python rather than trusting the file's arithmetic:",[30,1448,1450],{"className":295,"code":1449,"language":297,"meta":35,"style":35},"inv = pd.read_excel(\"uploads\u002Finvoice.xlsx\")\ninv[\"Total (recomputed)\"] = inv[\"Qty\"] * inv[\"Price\"]\nmismatch = inv[inv[\"Total\"] != inv[\"Total (recomputed)\"]]\nprint(mismatch[[\"Item\", \"Total\", \"Total (recomputed)\"]].to_string(index=False))\n",[14,1451,1452,1466,1497,1522],{"__ignoreMap":35},[39,1453,1454,1457,1459,1462,1464],{"class":41,"line":42},[39,1455,1456],{"class":308},"inv ",[39,1458,422],{"class":304},[39,1460,1461],{"class":308}," pd.read_excel(",[39,1463,1397],{"class":49},[39,1465,617],{"class":308},[39,1467,1468,1471,1474,1477,1479,1482,1485,1487,1490,1492,1495],{"class":41,"line":318},[39,1469,1470],{"class":308},"inv[",[39,1472,1473],{"class":49},"\"Total (recomputed)\"",[39,1475,1476],{"class":308},"] ",[39,1478,422],{"class":304},[39,1480,1481],{"class":308}," inv[",[39,1483,1484],{"class":49},"\"Qty\"",[39,1486,1476],{"class":308},[39,1488,1489],{"class":304},"*",[39,1491,1481],{"class":308},[39,1493,1494],{"class":49},"\"Price\"",[39,1496,487],{"class":308},[39,1498,1499,1502,1504,1507,1510,1512,1515,1517,1519],{"class":41,"line":325},[39,1500,1501],{"class":308},"mismatch ",[39,1503,422],{"class":304},[39,1505,1506],{"class":308}," inv[inv[",[39,1508,1509],{"class":49},"\"Total\"",[39,1511,1476],{"class":308},[39,1513,1514],{"class":304},"!=",[39,1516,1481],{"class":308},[39,1518,1473],{"class":49},[39,1520,1521],{"class":308},"]]\n",[39,1523,1524,1526,1529,1532,1534,1536,1538,1540,1543,1545,1547,1549],{"class":41,"line":349},[39,1525,1140],{"class":328},[39,1527,1528],{"class":308},"(mismatch[[",[39,1530,1531],{"class":49},"\"Item\"",[39,1533,266],{"class":308},[39,1535,1509],{"class":49},[39,1537,266],{"class":308},[39,1539,1473],{"class":49},[39,1541,1542],{"class":308},"]].to_string(",[39,1544,1403],{"class":800},[39,1546,422],{"class":304},[39,1548,1408],{"class":328},[39,1550,821],{"class":308},[25,1552,1554],{"id":1553},"step-5-parse-in-a-separate-process-with-limits","Step 5 — Parse in a separate process with limits",[10,1556,1557,1558,1561],{},"Checks catch known problems; limits contain unknown ones. Parse each upload in a child process with a memory ceiling and a timeout, so a file that defeats the checks costs one failed job, not the whole service. On Linux and macOS, ",[14,1559,1560],{},"resource.setrlimit"," caps the child's address space:",[30,1563,1565],{"className":295,"code":1564,"language":297,"meta":35,"style":35},"import json\nimport os\nimport resource\nimport subprocess\nimport sys\n\ndef parse_limited(path, max_mb=1024, timeout=30):\n    code = (\"import sys, pandas as pd; df = pd.read_excel(sys.argv[1], engine='openpyxl', nrows=100_000); \"\n            \"print(df.to_json(orient='records'))\")\n    limit = max_mb * 1024 * 1024\n    def cap():\n        resource.setrlimit(resource.RLIMIT_AS, (limit, limit))\n    try:\n        out = subprocess.run([sys.executable, \"-c\", code, str(path)], capture_output=True, text=True,\n                             timeout=timeout, preexec_fn=cap,\n                             env=dict(os.environ, OPENBLAS_NUM_THREADS=\"1\"))\n    except subprocess.TimeoutExpired:\n        return None, \"timed out\"\n    if out.returncode != 0:\n        return None, out.stderr.strip().splitlines()[-1][:100]\n    return json.loads(out.stdout), \"ok\"\n\nrows, status = parse_limited(Path(\"uploads\u002Forder_ok.xlsx\"))\nprint(status, rows)\n",[14,1566,1567,1574,1581,1588,1595,1602,1606,1631,1644,1651,1669,1680,1691,1698,1738,1756,1779,1787,1798,1812,1834,1844,1848,1863],{"__ignoreMap":35},[39,1568,1569,1571],{"class":41,"line":42},[39,1570,312],{"class":304},[39,1572,1573],{"class":308}," json\n",[39,1575,1576,1578],{"class":41,"line":318},[39,1577,312],{"class":304},[39,1579,1580],{"class":308}," os\n",[39,1582,1583,1585],{"class":41,"line":325},[39,1584,312],{"class":304},[39,1586,1587],{"class":308}," resource\n",[39,1589,1590,1592],{"class":41,"line":349},[39,1591,312],{"class":304},[39,1593,1594],{"class":308}," subprocess\n",[39,1596,1597,1599],{"class":41,"line":398},[39,1598,312],{"class":304},[39,1600,1601],{"class":308}," sys\n",[39,1603,1604],{"class":41,"line":403},[39,1605,322],{"emptyLinePlaceholder":321},[39,1607,1608,1610,1613,1616,1618,1621,1624,1626,1629],{"class":41,"line":416},[39,1609,406],{"class":304},[39,1611,1612],{"class":409}," parse_limited",[39,1614,1615],{"class":308},"(path, max_mb",[39,1617,422],{"class":304},[39,1619,1620],{"class":328},"1024",[39,1622,1623],{"class":308},", timeout",[39,1625,422],{"class":304},[39,1627,1628],{"class":328},"30",[39,1630,1065],{"class":308},[39,1632,1633,1636,1638,1641],{"class":41,"line":428},[39,1634,1635],{"class":308},"    code ",[39,1637,422],{"class":304},[39,1639,1640],{"class":308}," (",[39,1642,1643],{"class":49},"\"import sys, pandas as pd; df = pd.read_excel(sys.argv[1], engine='openpyxl', nrows=100_000); \"\n",[39,1645,1646,1649],{"class":41,"line":446},[39,1647,1648],{"class":49},"            \"print(df.to_json(orient='records'))\"",[39,1650,617],{"class":308},[39,1652,1653,1656,1658,1661,1663,1665,1667],{"class":41,"line":473},[39,1654,1655],{"class":308},"    limit ",[39,1657,422],{"class":304},[39,1659,1660],{"class":308}," max_mb ",[39,1662,1489],{"class":304},[39,1664,341],{"class":328},[39,1666,338],{"class":304},[39,1668,346],{"class":328},[39,1670,1671,1674,1677],{"class":41,"line":490},[39,1672,1673],{"class":304},"    def",[39,1675,1676],{"class":409}," cap",[39,1678,1679],{"class":308},"():\n",[39,1681,1682,1685,1688],{"class":41,"line":508},[39,1683,1684],{"class":308},"        resource.setrlimit(resource.",[39,1686,1687],{"class":328},"RLIMIT_AS",[39,1689,1690],{"class":308},", (limit, limit))\n",[39,1692,1693,1696],{"class":41,"line":517},[39,1694,1695],{"class":304},"    try",[39,1697,443],{"class":308},[39,1699,1700,1703,1705,1708,1711,1714,1716,1719,1722,1724,1727,1729,1731,1733,1735],{"class":41,"line":526},[39,1701,1702],{"class":308},"        out ",[39,1704,422],{"class":304},[39,1706,1707],{"class":308}," subprocess.run([sys.executable, ",[39,1709,1710],{"class":49},"\"-c\"",[39,1712,1713],{"class":308},", code, ",[39,1715,1208],{"class":328},[39,1717,1718],{"class":308},"(path)], ",[39,1720,1721],{"class":800},"capture_output",[39,1723,422],{"class":304},[39,1725,1726],{"class":328},"True",[39,1728,266],{"class":308},[39,1730,115],{"class":800},[39,1732,422],{"class":304},[39,1734,1726],{"class":328},[39,1736,1737],{"class":308},",\n",[39,1739,1740,1743,1745,1748,1751,1753],{"class":41,"line":556},[39,1741,1742],{"class":800},"                             timeout",[39,1744,422],{"class":304},[39,1746,1747],{"class":308},"timeout, ",[39,1749,1750],{"class":800},"preexec_fn",[39,1752,422],{"class":304},[39,1754,1755],{"class":308},"cap,\n",[39,1757,1758,1761,1763,1766,1769,1772,1774,1777],{"class":41,"line":561},[39,1759,1760],{"class":800},"                             env",[39,1762,422],{"class":304},[39,1764,1765],{"class":328},"dict",[39,1767,1768],{"class":308},"(os.environ, ",[39,1770,1771],{"class":800},"OPENBLAS_NUM_THREADS",[39,1773,422],{"class":304},[39,1775,1776],{"class":49},"\"1\"",[39,1778,821],{"class":308},[39,1780,1781,1784],{"class":41,"line":584},[39,1782,1783],{"class":304},"    except",[39,1785,1786],{"class":308}," subprocess.TimeoutExpired:\n",[39,1788,1789,1791,1793,1795],{"class":41,"line":948},[39,1790,449],{"class":304},[39,1792,1248],{"class":328},[39,1794,266],{"class":308},[39,1796,1797],{"class":49},"\"timed out\"\n",[39,1799,1800,1802,1805,1807,1810],{"class":41,"line":975},[39,1801,431],{"class":304},[39,1803,1804],{"class":308}," out.returncode ",[39,1806,1514],{"class":304},[39,1808,1809],{"class":328}," 0",[39,1811,443],{"class":308},[39,1813,1814,1816,1818,1821,1824,1826,1829,1832],{"class":41,"line":985},[39,1815,449],{"class":304},[39,1817,1248],{"class":328},[39,1819,1820],{"class":308},", out.stderr.strip().splitlines()[",[39,1822,1823],{"class":304},"-",[39,1825,818],{"class":328},[39,1827,1828],{"class":308},"][:",[39,1830,1831],{"class":328},"100",[39,1833,487],{"class":308},[39,1835,1836,1838,1841],{"class":41,"line":1007},[39,1837,529],{"class":304},[39,1839,1840],{"class":308}," json.loads(out.stdout), ",[39,1842,1843],{"class":49},"\"ok\"\n",[39,1845,1846],{"class":41,"line":1017},[39,1847,322],{"emptyLinePlaceholder":321},[39,1849,1850,1853,1855,1858,1861],{"class":41,"line":1025},[39,1851,1852],{"class":308},"rows, status ",[39,1854,422],{"class":304},[39,1856,1857],{"class":308}," parse_limited(Path(",[39,1859,1860],{"class":49},"\"uploads\u002Forder_ok.xlsx\"",[39,1862,821],{"class":308},[39,1864,1865,1867],{"class":41,"line":1030},[39,1866,1140],{"class":328},[39,1868,1869],{"class":308},"(status, rows)\n",[10,1871,1872,1873,1877,1878,1881,1882,1885],{},"The address-space limit counts memory the process ",[1874,1875,1876],"em",{},"reserves",", not only what it uses, and numeric libraries reserve buffers per thread when they load: with the default thread count, NumPy's OpenBLAS failed to start under a 512 MB cap here. Limiting it to one thread, as above, keeps the cap meaningful without breaking pandas. ",[14,1879,1880],{},"nrows"," bounds the work even for a legitimate but enormous sheet. On Windows, where ",[14,1883,1884],{},"resource"," is not available, run the parser in a container or a job object with a memory limit instead.",[67,1887,76,1893,76,1896,76,1899,76,1904,76,1910,76,1916,76,1920,76,1924,76,1928,76,1932,76,1935,76,1940,76,1942,76,1945,76,1947,76,1951,76,1954,76,1956,76,1958,76,1961,76,1963,76,1965,76,1968,76,1972,76,1974,76,1976,76,1978,76,1980,76,1982,76,1985,76,1987,76,1990,76,1992,76,1996,76,1999,76,2002,76,2004,76,2006],{"viewBox":1888,"role":70,"ariaLabelledBy":1889,"xmlns":74,"style":1892},"192 0 375 200",[1890,1891],"ut-results-t","ut-results-d","width:100%;max-width:375px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[78,1894,1895],{"id":1890},"The inbox after the checks",[82,1897,1898],{"id":1891},"A table of the five uploaded files and the check that stopped each one. The portal export failed the signature check, the bomb failed the compression ratio check, the entities file was refused by defusedxml, the invoice parsed but its total failed recomputation, and the normal order passed every check.",[86,1900],{"x":1901,"y":89,"width":1902,"height":1903,"fill":92},"192","375","200",[86,1905],{"x":1906,"y":96,"width":1907,"height":1908,"rx":89,"fill":144,"stroke":135,"style":1909},"208.7","158.8","28.0","stroke-width:1.5px",[115,1911,1915],{"x":1912,"y":1913,"style":1914},"288.1","35.0","font-size:11px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","File",[86,1917],{"x":1918,"y":96,"width":1919,"height":1908,"rx":89,"fill":144,"stroke":135,"style":1909},"367.4","107.0",[115,1921,1923],{"x":1922,"y":1913,"style":1914},"421.0","Stopped by",[86,1925],{"x":1926,"y":96,"width":1927,"height":1908,"rx":89,"fill":144,"stroke":135,"style":1909},"474.5","76.8",[115,1929,1931],{"x":1930,"y":1913,"style":1914},"512.9","Result",[86,1933],{"x":1906,"y":1934,"width":1907,"height":1908,"rx":89,"fill":92,"stroke":101,"style":1301},"44.0",[115,1936,1939],{"x":1912,"y":1937,"style":1938},"63.0","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","export_from_portal.xlsx",[86,1941],{"x":1918,"y":1934,"width":1919,"height":1908,"rx":89,"fill":92,"stroke":101,"style":1301},[115,1943,1944],{"x":1922,"y":1937,"style":1938},"signature",[86,1946],{"x":1926,"y":1934,"width":1927,"height":1908,"rx":89,"fill":1318,"stroke":1319,"style":1909},[115,1948,1950],{"x":1930,"y":1937,"style":1949},"font-size:11px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","rejected",[86,1952],{"x":1906,"y":1953,"width":1907,"height":1908,"rx":89,"fill":92,"stroke":101,"style":1301},"72.0",[115,1955,645],{"x":1912,"y":1299,"style":1938},[86,1957],{"x":1918,"y":1953,"width":1919,"height":1908,"rx":89,"fill":92,"stroke":101,"style":1301},[115,1959,1960],{"x":1922,"y":1299,"style":1938},"ratio 934:1",[86,1962],{"x":1926,"y":1953,"width":1927,"height":1908,"rx":89,"fill":1318,"stroke":1319,"style":1909},[115,1964,1950],{"x":1930,"y":1299,"style":1949},[86,1966],{"x":1906,"y":1967,"width":1907,"height":1908,"rx":89,"fill":92,"stroke":101,"style":1301},"100.0",[115,1969,1971],{"x":1912,"y":1970,"style":1938},"119.0","entities.xlsx",[86,1973],{"x":1918,"y":1967,"width":1919,"height":1908,"rx":89,"fill":92,"stroke":101,"style":1301},[115,1975,192],{"x":1922,"y":1970,"style":1938},[86,1977],{"x":1926,"y":1967,"width":1927,"height":1908,"rx":89,"fill":1318,"stroke":1319,"style":1909},[115,1979,1950],{"x":1930,"y":1970,"style":1949},[86,1981],{"x":1906,"y":98,"width":1907,"height":1908,"rx":89,"fill":92,"stroke":101,"style":1301},[115,1983,1361],{"x":1912,"y":1984,"style":1938},"147.0",[86,1986],{"x":1918,"y":98,"width":1919,"height":1908,"rx":89,"fill":92,"stroke":101,"style":1301},[115,1988,1989],{"x":1922,"y":1984,"style":1938},"recomputation",[86,1991],{"x":1926,"y":98,"width":1927,"height":1908,"rx":89,"fill":236,"stroke":237,"style":1909},[115,1993,1995],{"x":1930,"y":1984,"style":1994},"font-size:11px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","flagged",[86,1997],{"x":1906,"y":1998,"width":1907,"height":1908,"rx":89,"fill":92,"stroke":101,"style":1301},"156.0",[115,2000,2001],{"x":1912,"y":1282,"style":1938},"order_ok.xlsx",[86,2003],{"x":1918,"y":1998,"width":1919,"height":1908,"rx":89,"fill":92,"stroke":101,"style":1301},[86,2005],{"x":1926,"y":1998,"width":1927,"height":1908,"rx":89,"fill":203,"stroke":204,"style":1909},[115,2007,2009],{"x":1930,"y":1282,"style":2008},"font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","accepted",[10,2011,2012],{},"Each file was stopped by the cheapest check that could catch it, and none of the rejected files reached a full parse. Keep a record of which check rejected what; a sudden run of rejections from one sender is worth a conversation, whether it is an attack or a misconfigured export.",[25,2014,2016],{"id":2015},"common-pitfalls","Common pitfalls",[2018,2019,2020,2028,2037,2043,2049],"ul",{},[2021,2022,2023,2027],"li",{},[2024,2025,2026],"strong",{},"Trusting the extension or the MIME type."," Both come from the sender; check the bytes.",[2021,2029,2030,2036],{},[2024,2031,2032,2033,2035],{},"Assuming ",[14,2034,192],{}," is installed."," openpyxl only uses it if present; assert it.",[2021,2038,2039,2042],{},[2024,2040,2041],{},"Reading cached formula values as facts."," Recompute totals from inputs.",[2021,2044,2045,2048],{},[2024,2046,2047],{},"Opening uploads in Excel \"to have a look\"."," That is the one step that can run macros; inspect with Python first.",[2021,2050,2051,2054],{},[2024,2052,2053],{},"Unbounded reads."," Always cap rows, columns, size and time for files you did not create.",[25,2056,2058],{"id":2057},"conclusion","Conclusion",[10,2060,2061,2062,2064],{},"Treat every uploaded workbook as untrusted input. Check the file signature and size, inspect the zip directory for oversized parts, extreme compression ratios, macros and external links before extracting anything, and make sure openpyxl runs with ",[14,2063,192],{},". Recompute important figures instead of trusting cached formula results, and parse in a separate process with memory, row and time limits so a hostile file fails alone.",[25,2066,2068],{"id":2067},"frequently-asked-questions","Frequently asked questions",[10,2070,2071,2074],{},[2024,2072,2073],{},"Is it safe to read an untrusted Excel file with pandas?","\npandas does not run macros or evaluate formulas, but it parses the zip and XML inside the file. Check size and compression first, install defusedxml for openpyxl, and parse with memory and time limits.",[10,2076,2077,2080],{},[2024,2078,2079],{},"Does openpyxl protect against XML bombs?","\nOnly when the defusedxml package is installed; openpyxl then refuses entity definitions. Check openpyxl.DEFUSEDXML is True in the environment that processes uploads.",[10,2082,2083,2086],{},[2024,2084,2085],{},"Can the values pandas reads from formulas be wrong?","\nYes. pandas reads the result stored in the file, which the file's author controls. Recalculate important figures from the input cells.",[10,2088,2089,2092],{},[2024,2090,2091],{},"How do I detect macros in an uploaded xlsm file?","\nLook for an xl\u002FvbaProject.bin part in the zip, or use oletools to extract and analyse the code without opening Excel.",[25,2094,2096],{"id":2095},"related","Related",[2018,2098,2099,2105,2112,2117,2122],{},[2021,2100,2101,2102,2104],{},"Up one level: ",[19,2103,22],{"href":21}," — making report jobs safe and reliable.",[2021,2106,2107,2111],{},[19,2108,2110],{"href":2109},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fprevent-formula-injection-in-excel-exports\u002F","Prevent Formula Injection in Excel Exports"," — the outbound side.",[2021,2113,2114,2116],{},[19,2115,1108],{"href":1107}," — reviewing macros safely.",[2021,2118,2119,2121],{},[19,2120,632],{"href":631}," — files that are not what they claim.",[2021,2123,2124,2128],{},[19,2125,2127],{"href":2126},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fignore-excel-lock-files-when-processing-a-folder\u002F","Ignore Excel Lock Files When Processing a Folder"," — other files that do not belong in the inbox.",[2130,2131,2132],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}",{"title":35,"searchDepth":318,"depth":318,"links":2134},[2135,2136,2137,2138,2139,2140,2141,2142,2143,2144],{"id":27,"depth":318,"text":28},{"id":259,"depth":318,"text":260},{"id":638,"depth":318,"text":639},{"id":1112,"depth":318,"text":1113},{"id":1347,"depth":318,"text":1348},{"id":1553,"depth":318,"text":1554},{"id":2015,"depth":318,"text":2016},{"id":2057,"depth":318,"text":2058},{"id":2067,"depth":318,"text":2068},{"id":2095,"depth":318,"text":2096},"2026-09-29","Accepting .xlsx uploads from customers or the web? Check what the file really is, reject decompression bombs before parsing, make sure openpyxl uses defusedxml, never trust cached formula results, spot macros and external links, and parse in a separate process with memory and time limits.","md",[2149,2151,2153,2155],{"q":2073,"a":2150},"pandas does not run macros or evaluate formulas, but it parses the zip and XML inside the file. Check size and compression first, install defusedxml for openpyxl, and parse with memory and time limits.",{"q":2079,"a":2152},"Only when the defusedxml package is installed; openpyxl then refuses entity definitions. Check openpyxl.DEFUSEDXML is True in the environment that processes uploads.",{"q":2085,"a":2154},"Yes. pandas reads the result stored in the file, which the file's author controls. Recalculate important figures from the input cells.",{"q":2091,"a":2156},"Look for an xl\u002FvbaProject.bin part in the zip, or use oletools to extract and analyse the code without opening Excel.","read untrusted excel files safely in python",null,{"breadcrumb":2160},[2161,2163,2166],{"name":2162,"item":810},"Home",{"name":2164,"item":2165},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":22,"item":21},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fread-untrusted-excel-files-safely-in-python",{"title":2169,"description":2170},"Read Untrusted Excel Files Safely in Python (openpyxl, pandas)","Safely process Excel files uploaded by users in Python: file signature checks, zip bomb limits, defusedxml with openpyxl, spoofed cached formula values, macros and external links, and sandboxed parsing with resource limits.","read-untrusted-excel-files-safely-in-python","automating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fread-untrusted-excel-files-safely-in-python\u002Findex",[2174,2175],"reliable-jobs","reading-excel","how-to","n3rRZojp1it5stTCbzvZbg8JwkQoJqvLxac_Kvx2QkE",[2179,2183],{"title":2180,"path":2181,"stem":2182,"children":-1},"Quarantine Bad Excel Files in a Batch Job","\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fquarantine-bad-excel-files-in-a-batch-job","automating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fquarantine-bad-excel-files-in-a-batch-job\u002Findex",{"title":2184,"path":2185,"stem":2186,"children":-1},"Retry a Failed Excel Report Job in Python","\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fretry-a-failed-excel-report-job-in-python","automating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fretry-a-failed-excel-report-job-in-python\u002Findex",[2188,2190],{"path":2189,"title":22},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation",{"path":2191,"title":2164},"\u002Fautomating-reporting-workflows",[],[2194,2198,2201,2204,2207,2210],{"path":2195,"title":2196,"kind":2197},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts","Testing and Packaging Excel Automation Scripts","guide",{"path":2199,"title":2200,"kind":2176},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fblock-large-or-sensitive-excel-files-with-a-pre-commit-hook","Block Large or Sensitive Excel Files with a Pre-commit Hook",{"path":2202,"title":2203,"kind":2176},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fbuild-a-command-line-tool-for-excel-reports-with-argparse","Build a Command-Line Tool for Excel Reports with argparse",{"path":2205,"title":2206,"kind":2176},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcompare-excel-output-to-a-golden-file-in-tests","Compare Excel Output to a Golden File in Tests",{"path":2208,"title":2209,"kind":2176},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcreate-excel-test-fixtures-in-memory","Create Excel Test Fixtures in Memory for pytest",{"path":2211,"title":2212,"kind":2176},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fdiff-excel-files-in-git-with-a-textconv-driver","Diff Excel Files in Git with a textconv Driver",1790972132123]