Skip to content

Commit d5d7b00

Browse files
committed
add a new skill
1 parent 5f433d8 commit d5d7b00

4 files changed

Lines changed: 247 additions & 46 deletions

File tree

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
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).

.claude/skills/secure-microsoft-word-validation/SKILL.md

Lines changed: 46 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@ Apply **all** rules below when generating or reviewing any code related to valid
1717
- ALWAYS ensure that the file use the file type named `DOCX`.
1818
- ALWAYS ensure that the file has a single extension and is `docx`.
1919
- 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.
2021
- ALWAYS ensure that the file has no Visual Basic for Application (VBA) macros.
2122
- ALWAYS ensure that the file has no Object Linking and Embedding (OLE) package.
23+
- ALWAYS ensure that the file has no Dynamic Data Exchange (DDE) fields.
24+
25+
> **Intentional scope limits:** remote template references and external linked-image/content relationships are **not** blocked by this skill so that legitimate Word templates and linked images remain usable. Apply network-level controls or a dedicated content-inspection layer to cover those vectors if needed.
2226
2327
```java
2428
// BAD: No validation is applied
@@ -44,7 +48,6 @@ public class UnsafeReadWordFile {
4448

4549
// GOOD: All points are validated
4650
import org.apache.poi.openxml4j.opc.*;
47-
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
4851
import org.apache.poi.xwpf.usermodel.*;
4952
import java.io.*;
5053

@@ -58,83 +61,80 @@ public class SafeWordFileReader {
5861
String name = file.getName();
5962
int dotCount = name.length() - name.replace(".", "").length();
6063
if (dotCount != 1) {
61-
throw new SecurityException(
62-
"File must have exactly one extension. Found: " + name);
64+
throw new SecurityException("File must have exactly one extension. Found: " + name);
6365
}
6466
if (!name.toLowerCase().endsWith(".docx")) {
65-
throw new SecurityException(
66-
"File extension must be '.docx'. Found: " + name);
67+
throw new SecurityException("File extension must be '.docx'. Found: " + name);
6768
}
6869

6970
// ── CHECK 2: File size must not exceed 5 MB ───────────────────────
7071
long maxSizeBytes = 5L * 1024 * 1024;
7172
if (file.length() > maxSizeBytes) {
72-
throw new SecurityException(
73-
"File size exceeds the maximum allowed size of 5 MB. " +
74-
"Found: " + file.length() + " bytes.");
73+
throw new SecurityException("File size exceeds the maximum allowed size of 5 MB. Found: " + file.length() + " bytes.");
7574
}
7675

7776
// ── CHECK 3: Office Open XML magic bytes (PK\x03\x04) ─────────────
7877
try (FileInputStream fis = new FileInputStream(file)) {
7978
byte[] header = new byte[4];
8079
int bytesRead = fis.read(header);
81-
if (bytesRead < 4
82-
|| header[0] != 0x50
83-
|| header[1] != 0x4B
84-
|| header[2] != 0x03
85-
|| header[3] != 0x04) {
86-
throw new SecurityException(
87-
"File is not a valid Office Open XML (OOXML/ZIP) file. " +
88-
"Magic bytes do not match PK\\x03\\x04.");
80+
if (bytesRead < 4 || header[0] != 0x50 || header[1] != 0x4B || header[2] != 0x03 || header[3] != 0x04) {
81+
throw new SecurityException("File is not a valid Office Open XML (OOXML/ZIP) file. Magic bytes do not match PK\\x03\\x04.");
82+
}
83+
}
84+
85+
// ── CHECK 4: ZIP bomb — total uncompressed size must not exceed 50 MB ──
86+
long maxUncompressedBytes = 50L * 1024 * 1024;
87+
long totalUncompressedSize = 0;
88+
try (java.util.zip.ZipFile zip = new java.util.zip.ZipFile(file)) {
89+
java.util.Enumeration<? extends java.util.zip.ZipEntry> entries = zip.entries();
90+
while (entries.hasMoreElements()) {
91+
java.util.zip.ZipEntry entry = entries.nextElement();
92+
long entrySize = entry.getSize();
93+
if (entrySize > 0) {
94+
totalUncompressedSize += entrySize;
95+
}
96+
if (totalUncompressedSize > maxUncompressedBytes) {
97+
throw new SecurityException("File total uncompressed size exceeds the maximum allowed size of 50 MB. Possible ZIP bomb detected.");
98+
}
8999
}
90100
}
91101

92-
// ── CHECK 4: Real DOCX (contains word/document.xml) ───────────────
93-
// ── CHECK 5: No VBA macros (no word/vbaProject.bin) ───────────────
94-
// ── CHECK 6: No embedded OLE/ActiveX objects ──────────────────────
102+
// ── CHECK 5: Real DOCX (contains word/document.xml) ───────────────
103+
// ── CHECK 6: No VBA macros (no word/vbaProject.bin) ───────────────
104+
// ── CHECK 7: No embedded OLE/ActiveX objects ──────────────────────
95105
String oleObjectUri = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject";
96106
String activeXUri = "http://schemas.microsoft.com/office/2006/relationships/activeX";
97107

98108
try (OPCPackage pkg = OPCPackage.open(file)) {
99-
if (pkg.getPartsByName(
100-
java.util.regex.Pattern.compile("/word/document\\.xml"))
101-
.isEmpty()) {
102-
throw new SecurityException(
103-
"File does not contain 'word/document.xml'. " +
104-
"It is not a valid DOCX file.");
109+
if (pkg.getPartsByName(java.util.regex.Pattern.compile("/word/document\\.xml")).isEmpty()) {
110+
throw new SecurityException("File does not contain 'word/document.xml'. It is not a valid DOCX file.");
105111
}
106112

107-
if (!pkg.getPartsByName(
108-
java.util.regex.Pattern.compile("(?i)/word/vbaProject\\.bin"))
109-
.isEmpty()) {
110-
throw new SecurityException(
111-
"File contains a VBA macro project (vbaProject.bin). " +
112-
"Macro-enabled documents (DOCM) are not allowed.");
113+
if (!pkg.getPartsByName(java.util.regex.Pattern.compile("(?i)/word/vbaProject\\.bin")).isEmpty()) {
114+
throw new SecurityException("File contains a VBA macro project (vbaProject.bin). Macro-enabled documents (DOCM) are not allowed.");
113115
}
114116

115117
for (PackagePart part : pkg.getParts()) {
116118
for (PackageRelationship rel : part.getRelationships()) {
117119
String relType = rel.getRelationshipType();
118120
if (relType != null) {
119121
String lower = relType.toLowerCase();
120-
if (lower.startsWith(oleObjectUri.toLowerCase())
121-
|| lower.startsWith(activeXUri.toLowerCase())) {
122-
throw new SecurityException(
123-
"File contains an embedded OLE object in part: " +
124-
part.getPartName() + ". OLE packages are not allowed.");
122+
if (lower.startsWith(oleObjectUri.toLowerCase()) || lower.startsWith(activeXUri.toLowerCase())) {
123+
throw new SecurityException("File contains an embedded OLE object in part: " + part.getPartName() + ". OLE packages are not allowed.");
125124
}
126125
}
127126
}
128127
}
129-
}
130128

131-
// ── CHECK 7: No raw OLE2 compound file (old .doc binary) ──────────
132-
try (FileInputStream fis = new FileInputStream(file)) {
133-
byte[] header = new byte[8];
134-
if (fis.read(header) >= 4 && POIFSFileSystem.hasPOIFSHeader(header)) {
135-
throw new SecurityException(
136-
"File appears to be an OLE2 compound document (old .doc binary format). " +
137-
"Only OOXML DOCX files are accepted.");
129+
// ── CHECK 8: No DDE fields ─────────────────────────────────────────────
130+
java.util.List<PackagePart> docParts = pkg.getPartsByName(java.util.regex.Pattern.compile("/word/document\\.xml"));
131+
if (!docParts.isEmpty()) {
132+
try (java.io.InputStream docIs = docParts.get(0).getInputStream()) {
133+
String docXml = new String(docIs.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8).toUpperCase();
134+
if (docXml.contains("DDEAUTO") || docXml.contains(">DDE ") || docXml.contains(">DDE<")) {
135+
throw new SecurityException("File contains DDE (Dynamic Data Exchange) fields. DDE fields are not allowed.");
136+
}
137+
}
138138
}
139139
}
140140

@@ -178,8 +178,10 @@ Before finalizing generated code, verify:
178178
- [ ] The file use the file type named `DOCX`.
179179
- [ ] The file has a single extension and is `docx`.
180180
- [ ] The file size does not exceed 5 megabytes.
181+
- [ ] The total uncompressed size of all ZIP entries does not exceed 50 megabytes.
181182
- [ ] The file has no Visual Basic for Application (VBA) macros.
182183
- [ ] The file has no Object Linking and Embedding (OLE) package.
184+
- [ ] The file has no Dynamic Data Exchange (DDE) fields.
183185

184186
## References
185187

0 commit comments

Comments
 (0)