Skip to content

Commit 7c9ac0d

Browse files
committed
Enhance existing skills and add one about email address validation.
1 parent b53f841 commit 7c9ac0d

6 files changed

Lines changed: 222 additions & 45 deletions

File tree

.claude/skills/secure-csv-generation/SKILL.md

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,44 +16,44 @@ Apply **all** rules below when generating or reviewing any code related to Comma
1616

1717
```java
1818
// BAD: Content of fields are not validated to detect and disable any CSV injection
19-
String filePath = "output.csv";
20-
String[][] rows = {{"Name", "=CMD|' /C calc'!A0"}, {"Bob", "+1234"}};
21-
try (PrintWriter writer = new PrintWriter(new FileWriter(filePath))) {
22-
for (String[] row : rows) {
23-
StringBuilder sb = new StringBuilder();
24-
for (int i = 0; i < row.length; i++) {
25-
String field = row[i] == null ? "" : row[i];
26-
if (field.contains(",") || field.contains("\"") || field.contains("\n"))
27-
field = "\"" + field.replace("\"", "\"\"") + "\"";
28-
if (i > 0) sb.append(",");
29-
sb.append(field);
30-
}
31-
writer.println(sb);
32-
}
33-
}
19+
String filePath = "output.csv";
20+
String[][] rows = {{"Name", "=CMD|' /C calc'!A0"}, {"Bob", "+1234"}};
21+
try (PrintWriter writer = new PrintWriter(new FileWriter(filePath))) {
22+
for (String[] row : rows) {
23+
StringBuilder sb = new StringBuilder();
24+
for (int i = 0; i < row.length; i++) {
25+
String field = row[i] == null ? "" : row[i];
26+
if (field.contains(",") || field.contains("\"") || field.contains("\n"))
27+
field = "\"" + field.replace("\"", "\"\"") + "\"";
28+
if (i > 0) sb.append(",");
29+
sb.append(field);
30+
}
31+
writer.println(sb);
32+
}
33+
}
3434

3535
// GOOD: Dangerous characters used in CSV injection are prefixed to disable the injection
36-
String filePath = "output.csv";
37-
String[][] rows = {{"Name", "=CMD|' /C calc'!A0"}, {"Bob", "+1234"}};
38-
try (PrintWriter writer = new PrintWriter(new FileWriter(filePath))) {
39-
for (String[] row : rows) {
40-
StringBuilder sb = new StringBuilder();
41-
for (int i = 0; i < row.length; i++) {
42-
String field = row[i] == null ? "" : row[i];
36+
String filePath = "output.csv";
37+
String[][] rows = {{"Name", "=CMD|' /C calc'!A0"}, {"Bob", "+1234"}};
38+
try (PrintWriter writer = new PrintWriter(new FileWriter(filePath))) {
39+
for (String[] row : rows) {
40+
StringBuilder sb = new StringBuilder();
41+
for (int i = 0; i < row.length; i++) {
42+
String field = row[i] == null ? "" : row[i];
4343

44-
// Prepend single quote to disable CSV injection
45-
if (!field.isEmpty() && "=+-@\t\r".indexOf(field.charAt(0)) >= 0)
46-
field = "'" + field;
44+
// Prepend single quote to disable CSV injection
45+
if (!field.isEmpty() && "=+-@\t\r".indexOf(field.charAt(0)) >= 0)
46+
field = "'" + field;
4747

48-
if (field.contains(",") || field.contains("\"") || field.contains("\n"))
49-
field = "\"" + field.replace("\"", "\"\"") + "\"";
48+
if (field.contains(",") || field.contains("\"") || field.contains("\n"))
49+
field = "\"" + field.replace("\"", "\"\"") + "\"";
5050

51-
if (i > 0) sb.append(",");
52-
sb.append(field);
53-
}
54-
writer.println(sb);
55-
}
51+
if (i > 0) sb.append(",");
52+
sb.append(field);
5653
}
54+
writer.println(sb);
55+
}
56+
}
5757
```
5858

5959
## 2. Output Checklist
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
---
2+
name: secure-email-validation
3+
description: Generate secure email address validation code. Enforces secure generation of code validating an email address. Invoke when writing any email address validation related code.
4+
allowed-tools: Read Grep Glob
5+
metadata:
6+
category: security
7+
---
8+
9+
# Secure Email Address Validation Code Generation Rules
10+
11+
Apply **all** rules below when generating or reviewing any code related to validation of an email address.
12+
13+
## 1. Email address validation (CRITICAL)
14+
15+
- ALWAYS ensure that the email address is a valid email address, from a parser perspective, following RFCs on email addresses.
16+
- ALWAYS ensure that the email address is not using "Encoded-word" format.
17+
- ALWAYS ensure that the email address is not using comment format.
18+
- ALWAYS ensure that the email address is not using "Punycode" format.
19+
- ALWAYS ensure that the email address is not using UUCP style addresses.
20+
- ALWAYS ensure that the email address is not using address literals.
21+
- ALWAYS ensure that the email address is not using source routes.
22+
- ALWAYS ensure that the email address is not using the "percent hack".
23+
- ALWAYS enforce RFC 5321 length limits: local part ≤ 64 characters, domain ≤ 255 characters, total address ≤ 320 characters.
24+
- ALWAYS ensure that the email address does not contain newline or carriage-return characters (CRLF injection prevention).
25+
- ALWAYS ensure that the domain part contains at least one dot (reject single-label domains such as localhost or internal hostnames).
26+
- ALWAYS ensure that the local part is not a quoted string (i.e. not wrapped in double quotes).
27+
28+
```java
29+
// BAD: No validation is applied
30+
import jakarta.mail.internet.InternetAddress;
31+
32+
public static InternetAddress readEmailInsecure(String address) throws AddressException {
33+
return new InternetAddress(address);
34+
}
35+
36+
// GOOD: All points are applied
37+
import jakarta.mail.internet.AddressException;
38+
import jakarta.mail.internet.InternetAddress;
39+
40+
public static InternetAddress readEmailSecure(String email) throws AddressException {
41+
if (email == null || email.isBlank()) {
42+
throw new AddressException("Email address must not be null or blank");
43+
}
44+
45+
// 1. Parse and validate via RFC-compliant parser
46+
InternetAddress address = new InternetAddress(email, true);
47+
address.validate();
48+
49+
String raw = address.getAddress();
50+
51+
// 2. No encoded-word format: =?charset?encoding?text?=
52+
if (email.contains("=?") && email.contains("?=")) {
53+
throw new AddressException("Encoded-word format is not allowed: " + email);
54+
}
55+
56+
// 3. No comment format: parentheses ( )
57+
if (raw.contains("(") || raw.contains(")")) {
58+
throw new AddressException("Comment format is not allowed: " + email);
59+
}
60+
61+
// 4. No Punycode format: xn-- in domain
62+
String domain = raw.substring(raw.lastIndexOf('@') + 1);
63+
if (domain.toLowerCase().contains("xn--")) {
64+
throw new AddressException("Punycode format is not allowed: " + email);
65+
}
66+
67+
// 5. No UUCP style addresses: bang paths using !
68+
if (raw.contains("!")) {
69+
throw new AddressException("UUCP-style addresses are not allowed: " + email);
70+
}
71+
72+
// 6. No address literals: domain in square brackets [...]
73+
if (domain.startsWith("[") && domain.endsWith("]")) {
74+
throw new AddressException("Address literals are not allowed: " + email);
75+
}
76+
77+
// 7. No source routes: input starts with @ (e.g. @relay:user@domain or @r1,@r2:user@domain)
78+
if (email.startsWith("@")) {
79+
throw new AddressException("Source routes are not allowed: " + email);
80+
}
81+
82+
// 8. No percent hack: % in the local part
83+
String localPart = raw.substring(0, raw.lastIndexOf('@'));
84+
if (localPart.contains("%")) {
85+
throw new AddressException("Percent hack is not allowed: " + email);
86+
}
87+
88+
// 9. RFC 5321 length limits
89+
if (localPart.length() > 64) {
90+
throw new AddressException("Local part exceeds 64 characters: " + email);
91+
}
92+
if (domain.length() > 255) {
93+
throw new AddressException("Domain exceeds 255 characters: " + email);
94+
}
95+
if (raw.length() > 320) {
96+
throw new AddressException("Email address exceeds 320 characters: " + email);
97+
}
98+
99+
// 10. No CRLF injection: newline or carriage-return characters
100+
if (email.contains("\n") || email.contains("\r")) {
101+
throw new AddressException("Newline characters are not allowed: " + email);
102+
}
103+
104+
// 11. No single-label domain: domain must contain at least one dot
105+
if (!domain.contains(".")) {
106+
throw new AddressException("Single-label domains are not allowed: " + email);
107+
}
108+
109+
// 12. No quoted local part: local part must not be wrapped in double quotes
110+
if (localPart.startsWith("\"") && localPart.endsWith("\"")) {
111+
throw new AddressException("Quoted local parts are not allowed: " + email);
112+
}
113+
114+
return address;
115+
}
116+
```
117+
118+
## 2. Output Checklist
119+
120+
Before finalizing generated code, verify:
121+
122+
- [ ] The email address is a valid email address, from a parser perspective, following RFCs on email addresses.
123+
- [ ] The email address is not using "Encoded-word" format.
124+
- [ ] The email address is not using comment format.
125+
- [ ] The email address is not using "Punycode" format.
126+
- [ ] The email address is not using UUCP style addresses.
127+
- [ ] The email address is not using address literals.
128+
- [ ] The email address is not using source routes.
129+
- [ ] The email address is not using the "percent hack".
130+
- [ ] The local part is ≤ 64 characters, the domain is ≤ 255 characters, and the total address is ≤ 320 characters (RFC 5321).
131+
- [ ] The email address does not contain newline or carriage-return characters.
132+
- [ ] The domain part contains at least one dot (no single-label domains).
133+
- [ ] The local part is not a quoted string (not wrapped in double quotes).
134+
135+
## References
136+
137+
- [Research on email address parser bypass from PortSwigger](https://portswigger.net/research/splitting-the-email-atom).
138+
- [MIME (Multipurpose Internet Mail Extensions) part three:Message Header extensions for Non-ASCII text from IETF](https://datatracker.ietf.org/doc/html/rfc2047).
139+
- [Syntax of encoded-words from IETF](https://datatracker.ietf.org/doc/html/rfc2047#section-2).
140+
- [Anatomy of an email address from Jochen Topf](https://www.jochentopf.com/email/address.html).
141+
- [Email address from Wikipedia](https://en.wikipedia.org/wiki/Email_address).
142+
- [RFC 5321 - Simple Mail Transfer Protocol (size limits) from IETF](https://datatracker.ietf.org/doc/html/rfc5321#section-4.5.3).

.claude/skills/secure-image-validation/SKILL.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ Apply **all** rules below when generating or reviewing any code related to valid
1717
- ALWAYS resize the image by removing 1px in width and 1px in height to remove any embedded code.
1818

1919
```java
20+
import java.awt.image.BufferedImage;
21+
import java.io.ByteArrayInputStream;
22+
import java.io.File;
23+
import java.io.IOException;
24+
import java.nio.file.Files;
25+
import java.nio.file.Path;
26+
import java.util.Arrays;
27+
import javax.imageio.ImageIO;
28+
2029
// BAD: No validation is applied
2130
File file = new File("image.png");
2231
BufferedImage image = ImageIO.read(file);

.claude/skills/secure-log-entry-generation/SKILL.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ Apply **all** rules below when generating or reviewing any code related to gener
1919

2020
```java
2121
// BAD: No validation is applied
22-
String userControlledContent = request.getParameter("input");
23-
Logger logger = Logger.getLogger(Main.class.getName());
24-
logger.info(userControlledContent);
22+
void handleRequest(HttpServletRequest request) {
23+
String userControlledContent = request.getParameter("input");
24+
Logger logger = Logger.getLogger(Main.class.getName());
25+
logger.info(userControlledContent);
26+
}
2527

2628
// GOOD: All points are applied
2729
public class LogHelper {

.claude/skills/secure-message-digest-generation/SKILL.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,13 @@ Apply **all** rules below when generating or reviewing any code related to messa
2020
- ALWAYS encode the raw digest bytes as a lowercase hexadecimal string — never return raw bytes or use Base64, as hex is the canonical, human-readable, and interoperable representation for message digests.
2121

2222
```java
23+
import java.nio.charset.StandardCharsets;
24+
import java.security.MessageDigest;
25+
import java.security.NoSuchAlgorithmException;
26+
import java.util.HexFormat;
27+
2328
// BAD: Weak algorithm, platform charset, no separator, raw bytes returned
24-
public byte[] computeDigestInsecure(Object[] values) {
29+
public byte[] computeDigestInsecure(Object[] values) throws NoSuchAlgorithmException {
2530
StringBuilder combined = new StringBuilder();
2631
for (Object value : values) {
2732
combined.append(value != null ? value.toString() : ""); // no separator — hash collision risk
@@ -32,7 +37,7 @@ public byte[] computeDigestInsecure(Object[] values) {
3237
}
3338

3439
// GOOD: All rules applied
35-
public String computeDigestSecure(Object[] values) {
40+
public String computeDigestSecure(Object[] values) throws NoSuchAlgorithmException {
3641
StringBuilder combined = new StringBuilder();
3742
for (Object value : values) {
3843
// Rule 5: explicit empty string for null/empty — never skip

.claude/skills/secure-xml-parsing/SKILL.md

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,45 @@ Apply **all** rules below when generating or reviewing any code related to xml c
1616
- ALWAYS disable resolution of XML External Entity.
1717
- ALWAYS disable expansion/replacement of XML Internal Entity.
1818
- ALWAYS disable XInclude support.
19+
- ALWAYS limit the size of the XML input to prevent memory exhaustion (reject inputs larger than 1 MB).
1920

2021
```java
21-
// BAD: DTD and External Entities are resolved / Internal Entities are replaced / XInclude support is left to default configuration
22+
// BAD: DTD and External Entities are resolved / Internal Entities are replaced / XInclude support is left to default / no size limit
23+
import org.w3c.dom.Document;
24+
import javax.xml.parsers.DocumentBuilder;
25+
import javax.xml.parsers.DocumentBuilderFactory;
26+
import java.io.File;
27+
2228
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
23-
DocumentBuilder builder = factory.newDocumentBuilder();
24-
Document doc = builder.parse(new File("data.xml"));
29+
DocumentBuilder builder = factory.newDocumentBuilder();
30+
Document doc = builder.parse(new File("data.xml"));
31+
32+
// GOOD: DTD and External Entities are not resolved / Internal Entities are not replaced / XInclude disabled / input size limited
33+
import org.w3c.dom.Document;
34+
import javax.xml.parsers.DocumentBuilder;
35+
import javax.xml.parsers.DocumentBuilderFactory;
36+
import java.io.*;
37+
import java.nio.file.*;
38+
39+
int MAX_XML_SIZE_BYTES = 1 * 1024 * 1024; // 1 MB
40+
byte[] xmlBytes = Files.readAllBytes(Path.of("data.xml"));
41+
if (xmlBytes.length > MAX_XML_SIZE_BYTES) {
42+
throw new IOException("XML input exceeds maximum allowed size of 1 MB");
43+
}
2544

26-
// GOOD: DTD and External Entities are not resolved / Internal Entities are not replaced / XInclude support is explicitly disabled
2745
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
2846
// Disable DTD resolution entirely
2947
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
30-
//Disable XML External Entity (XXE) resolution
48+
// Disable XML External Entity (XXE) resolution
3149
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
3250
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
3351
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
34-
//Disable replacement of XML Internal Entities
52+
// Disable replacement of XML Internal Entities
3553
factory.setExpandEntityReferences(false);
36-
// Process namespaces safely
54+
// Disable XInclude support
3755
factory.setXIncludeAware(false);
3856
DocumentBuilder builder = factory.newDocumentBuilder();
39-
Document doc = builder.parse(new File("data.xml"));
57+
Document doc = builder.parse(new ByteArrayInputStream(xmlBytes));
4058
```
4159

4260
## 2. Output Checklist
@@ -47,6 +65,7 @@ Before finalizing generated code, verify:
4765
- [ ] External Entities (general and parameter) are NOT resolved.
4866
- [ ] Internal Entities are NOT replaced.
4967
- [ ] XInclude support is explicitly disabled.
68+
- [ ] XML input size is limited to 1 MB before parsing.
5069

5170
## References
5271

0 commit comments

Comments
 (0)