Home / VBA & Automation / Macro Security and Best Practice

Macro Security and Best Practice

Automation & AI12 min readMicrosoft 365 for Windows

The problem

A colleague sends a workbook with a macro that "updates everything in one click". You open it, and Excel shows a security warning and refuses to run the macro. Is Excel being overcautious? It is not. A macro is a program: it can do anything you can do on your computer, such as read your files, delete them, send e-mail or start other programs, and it does it with your permissions. That power is what makes macros useful, and it is why they are one of the oldest ways to spread malicious software.

There is a second problem on the other side of the table. The macros you write yourself are, one day, run by someone else, on data you have never seen, months after you wrote them. A macro that works only because its author remembers what it does is a liability of a different kind.

This article covers both sides: how Excel protects you from other people's macros, how to read code before you trust it, and how to write macros that others can trust.

Why macros are blocked

Since 2022, Microsoft 365 Apps on Windows block macros in files that come from the internet. Windows marks a file with a Mark of the Web, an invisible label saying that it came from an untrusted zone, when it is downloaded with a browser or saved from an e-mail attachment. Excel reads the label and, for a file from the internet zone, does not allow its macros to run: a message bar appears, and the only way forward is a deliberate act.

If you know the file and its sender, right-click it in File Explorer, choose Properties and, on the General tab, select Unblock. This removes the label, and the next time the file is opened the usual macro settings apply. Do it only for files you trust: it is you who is taking the decision, not Excel. Files saved in a Trusted Location skip the check altogether, which suits a folder that only you can write to and is a poor idea for a shared download folder. The mark exists only on drives formatted as NTFS, and the rule does not apply to Excel for the web or Excel on a Mac.

The companion files of this site follow the same logic: the macros come as a plain-text .bas file that you import yourself, so nothing runs until you have had the chance to read the code.

The Trust Center settings

Under File > Options > Trust Center > Trust Center Settings > Macro Settings, Excel offers four choices. If the Developer tab is visible, Macro Security on that tab goes to the same place.

Two more entries on the same page matter for macros. Trust access to the VBA project object model is off by default, and it lets programs and macros read and change the VBA code of your workbooks: leave it off unless a tool you trust needs it. And a check box controls whether Excel 4.0 macros, an old macro language, are allowed.

The right setting for almost everybody is the second one, and the habit that goes with it is to read the yellow bar as a question: do I know where this file came from? Old guides describe other levels, Low, Medium and High, that belonged to much older versions of Excel; those names no longer appear.

The Macro Settings page of the Trust Center with Disable VBA macros with notification selected.
The Macro Settings page of the Trust Center with Disable VBA macros with notification selected.

File formats

Macros need a macro-enabled format. The workbook type with macros is .xlsm. A normal .xlsx file cannot hold VBA code: if you save a workbook with macros as .xlsx, Excel warns you and, if you go on, the macros are lost. The binary format .xlsb can hold macros, and is a more compact format for very large workbooks; a macro-enabled add-in is .xlam. Tools outside Excel read .xlsx and .xlsm easily, while .xlsb is understood by few of them.

For the companion files of this site, which are .xlsx, the procedure is always the same: File > Save As, choose the macro-enabled format, then import the .bas file into the VBA editor.

Signing your macros

A digital signature does two things: it shows that the code has not changed since it was signed, and it lets a user or an administrator trust macros from a given publisher without enabling everything. In the VBA editor, Tools > Digital Signature lets you choose a certificate for the project.

For personal use or testing, Office includes a tool, SelfCert.exe, that creates a self-signed certificate. Microsoft states the limits: Office trusts such a certificate only on computers where it has been added to the trusted store, so it is fine for your own machine and a few you manage, and not a way to distribute macros to others. Within a company, the IT department can issue certificates that all computers trust. A signature proves that the code was not altered after signing, not that its author is honest; and any change to a signed project removes the signature until it is signed again.

Read before you run

The most useful security habit costs nothing: read the code before you run it. You do not need to understand every line to see that a macro formatting a price list has no reason to delete files or start programs. Some keywords deserve a second look in code that you did not write. The companion file has a sheet, Red Flags, with twelve of them and a plain-language note on what each one can do. These are the main ones.

Keyword What it can do
Kill Deletes files from disk
Shell Starts other programs
CreateObject Creates external objects: file system, scripting host, e-mail, web
SendKeys Types keystrokes into whatever window is active
Environ Reads system information such as the user name
Workbooks.Open, SaveAs Opens and writes other files
Declare Calls Windows functions outside VBA
Application.Run Runs macros stored in other workbooks
Auto_Open Runs by itself when the workbook opens

A keyword is not proof of harm: plenty of legitimate macros open and save files. It is a reason to read that line and ask why. On Error Resume Next is on the list for a different reason: not security but quality, since it hides every error that follows.

The macro Scan_Code_For_Red_Flags does the first pass for you. The sheet Code to Review holds 32 lines of sample VBA as plain text, and the sheet is only data: nothing on it is ever executed. The macro takes each keyword of Red Flags and loops through every line looking for it with InStr, counting the hits and noting the first line:

    For k = 3 To lastKey
        hits = 0
        For i = 2 To lastCode
            If InStr(1, wsC.Cells(i, 1).Value, wsF.Cells(k, 1).Value, vbTextCompare) > 0 Then
                hits = hits + 1
                If hits = 1 Then wsF.Cells(k, 4).Value = i       ' first line where it appears
            End If
        Next i
        wsF.Cells(k, 3).Value = hits
    Next k

The nested loops are the ones from the article on loops, and vbTextCompare makes the search ignore case. After the run, each keyword of the sample has one hit, except On Error Resume Next with two and URLDownloadToFile with none, and column E checks every count with a SUMPRODUCT formula. To scan code of your own, paste it into column A of Code to Review, one line per row, and extend the formula range.

Scan_Code_For_Red_Flags on the twelve keywords: the counts from the macro and from the formula agree.
Scan_Code_For_Red_Flags on the twelve keywords: the counts from the macro and from the formula agree.

With AI. Assistants now write VBA on request, and much of it is good. The rules above do not change because the author is a machine: read it, run it first on a copy and on dummy data, and use the same red-flag scan. Ask the assistant to explain each line you do not follow. An assistant also knows only what you told it, so check the references to sheets, ranges and file paths against your own workbook before you run anything.

Best practice: the same macro, written twice

The companion file has two macros that give the same answer. Both fill Summary!B3:B6 with the net sales of four markets from the sheet Sales, and the status cells beside them show Match. The totals are 1,306,205.55, 1,438,978.91, 1,336,398.64 and 1,041,193.64.

Build_Summary_Bad looks like what a recorded macro or a rushed one produces:

Sub Build_Summary_Bad()
    Application.ScreenUpdating = False
    Sheets("Summary").Select
    Range("B3").Select
    ActiveCell.Value = Application.WorksheetFunction.SumIf(Sheets("Sales").Range("B2:B121"), "Market A", Sheets("Sales").Range("E2:E121"))
    Range("B4").Select
    ' ... the same for Market B, C and D
End Sub

It works today, and fails in at least five ways tomorrow. It selects cells and sheets, which makes it slow and dependent on what is on the screen. It has the data range B2:B121 and E2:E121 written in the code, so a 121st row is ignored without a word. The market names are written in the code, so a fifth market needs a new macro. If an error occurs, screen updating stays off and Excel appears frozen. And nobody who opens the module understands what it is for without reading all of it.

Build_Summary is the same job written to be trusted:

Sub Build_Summary()
    Const SALES_SHEET As String = "Sales"
    Const SUMMARY_SHEET As String = "Summary"
    Const FIRST_MARKET_ROW As Long = 3

    Dim wsS As Worksheet, wsM As Worksheet
    Dim lastSales As Long, lastMarket As Long, r As Long
    Dim wasUpdating As Boolean

    wasUpdating = Application.ScreenUpdating
    On Error GoTo Cleanup
    Application.ScreenUpdating = False

    Set wsS = ThisWorkbook.Worksheets(SALES_SHEET)
    Set wsM = ThisWorkbook.Worksheets(SUMMARY_SHEET)
    lastSales = wsS.Cells(wsS.Rows.Count, 1).End(xlUp).Row
    lastMarket = wsM.Cells(wsM.Rows.Count, 1).End(xlUp).Row
    wsM.Range("B" & FIRST_MARKET_ROW & ":B" & wsM.Rows.Count).ClearContents

    For r = FIRST_MARKET_ROW To lastMarket
        wsM.Cells(r, 2).Value = Application.WorksheetFunction.SumIf( _
            wsS.Range("B2:B" & lastSales), wsM.Cells(r, 1).Value, wsS.Range("E2:E" & lastSales))
    Next r

Cleanup:
    Application.ScreenUpdating = wasUpdating
    If Err.Number <> 0 Then MsgBox "Build_Summary stopped: " & Err.Description, vbExclamation
End Sub

Every difference is a habit worth adopting.

Other habits that cannot be shown in one macro belong to the same list. Give variables names that say what they hold, CostCenter and not Variable1: in this site's examples, lastRow, threshold and wsPL. Keep procedures small and with one job, as seen with the function that calculates a retail price. Comment the reason for a line, not the line. Keep inputs on a sheet, in cells that users can see, and not in the code. And before a macro touches data that matters, save a copy: a macro cannot be undone with Ctrl+Z, so the copy is the undo button.

Build_Summary_Bad above, Build_Summary below: the second reads its own limits from the sheet.
Build_Summary_Bad above, Build_Summary below: the second reads its own limits from the sheet.
Market totals from Build_Summary beside the SUMIF check: all four match.
Market totals from Build_Summary beside the SUMIF check: all four match.

A last look before you share

The sheet Checklist condenses the article into nine lines, five for the person running a macro and four for the person writing one. In short: before you run a macro you did not write, ask where the file comes from, read the code, and run it on a copy. Before you share one of your own, save it in a macro-enabled format, declare your variables, keep addresses and paths out of the code, restore what you change, and tell the user what the macro does and that it cannot be undone.

Common mistakes

Companion files

Illustrative data only. Not a decision tool.

For further reading