|
| 1 | +--- |
| 2 | +name: secure-microsoft-excel-validation |
| 3 | +description: Generate secure microsoft excel file validation code. Enforces secure generation of code validating a microsoft excel file. Invoke when writing any microsoft excel file validation related code. |
| 4 | +allowed-tools: Read Grep Glob |
| 5 | +metadata: |
| 6 | + category: security |
| 7 | +--- |
| 8 | + |
| 9 | +# Secure Microsoft Excel File Validation Code Generation Rules |
| 10 | + |
| 11 | +Apply **all** rules below when generating or reviewing any code related to validation of a Microsoft Excel file. |
| 12 | + |
| 13 | +## 1. Microsoft Excel file validation (CRITICAL) |
| 14 | + |
| 15 | +- ALWAYS ensure that the file is a real Microsoft Excel file. |
| 16 | +- ALWAYS ensure that the file use the standard named `Office Open XML`. |
| 17 | +- ALWAYS ensure that the file use the file type named `XLSX`. |
| 18 | +- ALWAYS ensure that the file has a single extension and is `xlsx`. |
| 19 | +- ALWAYS ensure that the file size does not exceed 5 megabytes before opening or parsing it. |
| 20 | +- ALWAYS ensure that the total uncompressed size of all ZIP entries does not exceed 50 megabytes before opening or parsing it. |
| 21 | +- ALWAYS ensure that the file has no Visual Basic for Application (VBA) macros. |
| 22 | +- ALWAYS ensure that the file has no Object Linking and Embedding (OLE) package. |
| 23 | +- ALWAYS ensure that the file has no external data connections. |
| 24 | +- ALWAYS ensure that the file has no external links. |
| 25 | + |
| 26 | +```java |
| 27 | +// BAD: No validation is applied |
| 28 | +import org.apache.poi.ss.usermodel.*; |
| 29 | +import org.apache.poi.xssf.usermodel.*; |
| 30 | +import java.io.*; |
| 31 | + |
| 32 | +public class UnsafeReadExcelFile { |
| 33 | + public static void main(String[] args) { |
| 34 | + try (FileInputStream fis = new FileInputStream("spreadsheet.xlsx"); |
| 35 | + XSSFWorkbook workbook = new XSSFWorkbook(fis)) { |
| 36 | + XSSFSheet sheet = workbook.getSheetAt(0); |
| 37 | + for (Row row : sheet) { |
| 38 | + for (Cell cell : row) { |
| 39 | + System.out.print(cell + "\t"); |
| 40 | + } |
| 41 | + System.out.println(); |
| 42 | + } |
| 43 | + } catch (IOException e) { |
| 44 | + e.printStackTrace(); |
| 45 | + } |
| 46 | + } |
| 47 | +} |
| 48 | + |
| 49 | +// GOOD: All points are validated |
| 50 | +import org.apache.poi.openxml4j.opc.*; |
| 51 | +import org.apache.poi.ss.usermodel.*; |
| 52 | +import org.apache.poi.xssf.usermodel.*; |
| 53 | +import java.io.*; |
| 54 | + |
| 55 | +public class SafeExcelFileReader { |
| 56 | + |
| 57 | + public static void main(String[] args) { |
| 58 | + try { |
| 59 | + File file = new File("spreadsheet.xlsx"); |
| 60 | + |
| 61 | + // ── CHECK 1: Single extension and must be "xlsx" ────────────────── |
| 62 | + String name = file.getName(); |
| 63 | + int dotCount = name.length() - name.replace(".", "").length(); |
| 64 | + if (dotCount != 1) { |
| 65 | + throw new SecurityException("File must have exactly one extension. Found: " + name); |
| 66 | + } |
| 67 | + if (!name.toLowerCase().endsWith(".xlsx")) { |
| 68 | + throw new SecurityException("File extension must be '.xlsx'. Found: " + name); |
| 69 | + } |
| 70 | + |
| 71 | + // ── CHECK 2: File size must not exceed 5 MB ─────────────────────── |
| 72 | + long maxSizeBytes = 5L * 1024 * 1024; |
| 73 | + if (file.length() > maxSizeBytes) { |
| 74 | + throw new SecurityException("File size exceeds the maximum allowed size of 5 MB. Found: " + file.length() + " bytes."); |
| 75 | + } |
| 76 | + |
| 77 | + // ── CHECK 3: Office Open XML magic bytes (PK\x03\x04) ───────────── |
| 78 | + try (FileInputStream fis = new FileInputStream(file)) { |
| 79 | + byte[] header = new byte[4]; |
| 80 | + int bytesRead = fis.read(header); |
| 81 | + if (bytesRead < 4 || header[0] != 0x50 || header[1] != 0x4B || header[2] != 0x03 || header[3] != 0x04) { |
| 82 | + throw new SecurityException("File is not a valid Office Open XML (OOXML/ZIP) file. Magic bytes do not match PK\\x03\\x04."); |
| 83 | + } |
| 84 | + } |
| 85 | + |
| 86 | + // ── CHECK 4: ZIP bomb — total uncompressed size must not exceed 50 MB ── |
| 87 | + long maxUncompressedBytes = 50L * 1024 * 1024; |
| 88 | + long totalUncompressedSize = 0; |
| 89 | + try (java.util.zip.ZipFile zip = new java.util.zip.ZipFile(file)) { |
| 90 | + java.util.Enumeration<? extends java.util.zip.ZipEntry> entries = zip.entries(); |
| 91 | + while (entries.hasMoreElements()) { |
| 92 | + java.util.zip.ZipEntry entry = entries.nextElement(); |
| 93 | + long entrySize = entry.getSize(); |
| 94 | + if (entrySize > 0) { |
| 95 | + totalUncompressedSize += entrySize; |
| 96 | + } |
| 97 | + if (totalUncompressedSize > maxUncompressedBytes) { |
| 98 | + throw new SecurityException("File total uncompressed size exceeds the maximum allowed size of 50 MB. Possible ZIP bomb detected."); |
| 99 | + } |
| 100 | + } |
| 101 | + } |
| 102 | + |
| 103 | + // ── CHECK 5: Real XLSX (contains xl/workbook.xml) ───────────────── |
| 104 | + // ── CHECK 6: No VBA macros (no xl/vbaProject.bin) ───────────────── |
| 105 | + // ── CHECK 7: No embedded OLE/ActiveX objects ────────────────────── |
| 106 | + // ── CHECK 8: No external data connections ───────────────────────── |
| 107 | + // ── CHECK 9: No external links ──────────────────────────────────── |
| 108 | + String oleObjectUri = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject"; |
| 109 | + String activeXUri = "http://schemas.microsoft.com/office/2006/relationships/activeX"; |
| 110 | + |
| 111 | + try (OPCPackage pkg = OPCPackage.open(file)) { |
| 112 | + if (pkg.getPartsByName(java.util.regex.Pattern.compile("/xl/workbook\\.xml")).isEmpty()) { |
| 113 | + throw new SecurityException("File does not contain 'xl/workbook.xml'. It is not a valid XLSX file."); |
| 114 | + } |
| 115 | + |
| 116 | + if (!pkg.getPartsByName(java.util.regex.Pattern.compile("(?i)/xl/vbaProject\\.bin")).isEmpty()) { |
| 117 | + throw new SecurityException("File contains a VBA macro project (vbaProject.bin). Macro-enabled workbooks (XLSM) are not allowed."); |
| 118 | + } |
| 119 | + |
| 120 | + if (!pkg.getPartsByName(java.util.regex.Pattern.compile("(?i)/xl/connections\\.xml")).isEmpty()) { |
| 121 | + throw new SecurityException("File contains external data connections (connections.xml). External data connections are not allowed."); |
| 122 | + } |
| 123 | + |
| 124 | + if (!pkg.getPartsByName(java.util.regex.Pattern.compile("(?i)/xl/externalLinks/.*")).isEmpty()) { |
| 125 | + throw new SecurityException("File contains external links (externalLinks/). External links are not allowed."); |
| 126 | + } |
| 127 | + |
| 128 | + for (PackagePart part : pkg.getParts()) { |
| 129 | + for (PackageRelationship rel : part.getRelationships()) { |
| 130 | + String relType = rel.getRelationshipType(); |
| 131 | + if (relType != null) { |
| 132 | + String lower = relType.toLowerCase(); |
| 133 | + if (lower.startsWith(oleObjectUri.toLowerCase()) || lower.startsWith(activeXUri.toLowerCase())) { |
| 134 | + throw new SecurityException("File contains an embedded OLE object in part: " + part.getPartName() + ". OLE packages are not allowed."); |
| 135 | + } |
| 136 | + } |
| 137 | + } |
| 138 | + } |
| 139 | + } |
| 140 | + |
| 141 | + // ── READ: Sheet data ─────────────────────────────────────────────── |
| 142 | + try (FileInputStream fis = new FileInputStream(file); |
| 143 | + XSSFWorkbook workbook = new XSSFWorkbook(fis)) { |
| 144 | + |
| 145 | + for (int i = 0; i < workbook.getNumberOfSheets(); i++) { |
| 146 | + XSSFSheet sheet = workbook.getSheetAt(i); |
| 147 | + System.out.println("=== Sheet: " + sheet.getSheetName() + " ==="); |
| 148 | + for (Row row : sheet) { |
| 149 | + for (Cell cell : row) { |
| 150 | + System.out.print(cell + "\t"); |
| 151 | + } |
| 152 | + System.out.println(); |
| 153 | + } |
| 154 | + } |
| 155 | + } |
| 156 | + |
| 157 | + } catch (SecurityException e) { |
| 158 | + System.err.println("Security validation failed: " + e.getMessage()); |
| 159 | + } catch (Exception e) { |
| 160 | + System.err.println("Error reading file: " + e.getMessage()); |
| 161 | + } |
| 162 | + } |
| 163 | +} |
| 164 | +``` |
| 165 | + |
| 166 | +## 2. Output Checklist |
| 167 | + |
| 168 | +Before finalizing generated code, verify: |
| 169 | + |
| 170 | +- [ ] The file is a real Microsoft Excel file. |
| 171 | +- [ ] The file use the standard named `Office Open XML`. |
| 172 | +- [ ] The file use the file type named `XLSX`. |
| 173 | +- [ ] The file has a single extension and is `xlsx`. |
| 174 | +- [ ] The file size does not exceed 5 megabytes. |
| 175 | +- [ ] The total uncompressed size of all ZIP entries does not exceed 50 megabytes. |
| 176 | +- [ ] The file has no Visual Basic for Application (VBA) macros. |
| 177 | +- [ ] The file has no Object Linking and Embedding (OLE) package. |
| 178 | +- [ ] The file has no external data connections. |
| 179 | +- [ ] The file has no external links. |
| 180 | + |
| 181 | +## References |
| 182 | + |
| 183 | +- [ECMA-376 - Office Open XML file formats](https://ecma-international.org/publications-and-standards/standards/ecma-376/). |
| 184 | +- [Learn about file formats](https://support.microsoft.com/en-us/office/learn-about-file-formats-56dc3b55-7681-402e-a727-c59fa0884b30). |
| 185 | +- [Open XML Formats and file name extensions](https://support.microsoft.com/en-us/office/open-xml-formats-and-file-name-extensions-5200d93c-3449-4380-8e11-31ef14555b18). |
| 186 | +- [OWASP File Upload Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html). |
| 187 | +- [MITRE ATT&CK T1059.005 - Visual Basic](https://attack.mitre.org/techniques/T1059/005/). |
| 188 | +- [MITRE ATT&CK T1566.001 - Spearphishing Attachment](https://attack.mitre.org/techniques/T1566/001/). |
| 189 | +- [MITRE ATT&CK T1048 - Exfiltration Over Alternative Protocol](https://attack.mitre.org/techniques/T1048/). |
| 190 | +- [Microsoft: Macros from the internet are blocked by default](https://learn.microsoft.com/en-us/deployoffice/security/internet-macros-blocked). |
0 commit comments