Skip to content

Commit 0c2df20

Browse files
authored
MAT-10127: add xss Validator (#358)
* MAT-10127: add xss Validator * MAT-10127: update version * MAT-10127: update version
1 parent 08e6c15 commit 0c2df20

5 files changed

Lines changed: 134 additions & 1 deletion

File tree

pom.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>gov.cms.madie</groupId>
88
<artifactId>madie-java-models</artifactId>
9-
<version>0.10.0-SNAPSHOT</version>
9+
<version>0.10.1-SNAPSHOT</version>
1010
<name>madie-java-models</name>
1111
<description>Java based models for MADiE microservices</description>
1212
<properties>
@@ -91,6 +91,11 @@
9191
<artifactId>commons-lang3</artifactId>
9292
<version>3.18.0</version>
9393
</dependency>
94+
<dependency>
95+
<groupId>org.jsoup</groupId>
96+
<artifactId>jsoup</artifactId>
97+
<version>1.15.3</version>
98+
</dependency>
9499
</dependencies>
95100

96101
<build>

src/main/java/gov/cms/madie/models/measure/Measure.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ public class Measure implements Serializable, Cloneable {
7777
private String revisionNumber;
7878
private String state;
7979

80+
@XssFilter
8081
@Indexed
8182
@NotBlank(
8283
groups = {ValidationOrder1.class},
@@ -98,6 +99,7 @@ public class Measure implements Serializable, Cloneable {
9899
message = "eCQM Abbreviated Title cannot be more than 32 characters.")
99100
private String ecqmTitle;
100101

102+
@XssFilter
101103
@NotBlank(
102104
groups = {ValidationOrder1.class},
103105
message = "Measure Name is required.")
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package gov.cms.madie.models.validators;
2+
3+
import jakarta.validation.Constraint;
4+
import jakarta.validation.Payload;
5+
6+
import java.lang.annotation.*;
7+
8+
@Target({ElementType.FIELD})
9+
@Retention(RetentionPolicy.RUNTIME)
10+
@Constraint(validatedBy = XssValidator.class)
11+
@Documented
12+
public @interface XssFilter {
13+
String message() default "Content contains invalid characters.";
14+
15+
Class<?>[] groups() default {};
16+
17+
Class<? extends Payload>[] payload() default {};
18+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package gov.cms.madie.models.validators;
2+
3+
import jakarta.validation.ConstraintValidator;
4+
import jakarta.validation.ConstraintValidatorContext;
5+
import org.jsoup.Jsoup;
6+
import org.jsoup.safety.Safelist;
7+
8+
import java.net.URLDecoder;
9+
import java.nio.charset.StandardCharsets;
10+
import java.util.regex.Pattern;
11+
12+
public class XssValidator implements ConstraintValidator<XssFilter, String> {
13+
14+
private static final Pattern EXPRESSION_PATTERN = Pattern.compile(".*[\\{\\}\\$\\#\\[\\]\\*].*");
15+
16+
@Override
17+
public boolean isValid(String field, ConstraintValidatorContext context) {
18+
if (field != null) {
19+
20+
String decoded = URLDecoder.decode(field, StandardCharsets.UTF_8);
21+
22+
if (EXPRESSION_PATTERN.matcher(decoded).matches()) {
23+
return false;
24+
}
25+
26+
return Jsoup.isValid(decoded, Safelist.none());
27+
}
28+
return true;
29+
}
30+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package gov.cms.madie.models.validators;
2+
3+
import jakarta.validation.ConstraintValidatorContext;
4+
import org.junit.jupiter.api.BeforeEach;
5+
import org.junit.jupiter.api.Test;
6+
import org.junit.jupiter.api.extension.ExtendWith;
7+
import org.junit.jupiter.params.ParameterizedTest;
8+
import org.junit.jupiter.params.provider.ValueSource;
9+
import org.mockito.Mock;
10+
import org.mockito.junit.jupiter.MockitoExtension;
11+
12+
import static org.junit.jupiter.api.Assertions.assertFalse;
13+
import static org.junit.jupiter.api.Assertions.assertTrue;
14+
15+
@ExtendWith(MockitoExtension.class)
16+
class XssValidatorTest {
17+
18+
private XssValidator xssValidator;
19+
20+
@Mock
21+
private ConstraintValidatorContext context;
22+
23+
@BeforeEach
24+
void setUp() {
25+
xssValidator = new XssValidator();
26+
}
27+
28+
@Test
29+
void testIsValidReturnsTrueForNullInput() {
30+
assertTrue(xssValidator.isValid(null, context));
31+
}
32+
33+
@ParameterizedTest
34+
@ValueSource(strings = {
35+
"",
36+
" ",
37+
"Just a regular string with no tags",
38+
"Medicare Advantage Plan 2026",
39+
"This is safe alphanumeric input 123"
40+
})
41+
void testIsValidReturnsTrueForSafeStrings(String safeInput) {
42+
assertTrue(xssValidator.isValid(safeInput, context));
43+
}
44+
45+
@ParameterizedTest
46+
@ValueSource(strings = {
47+
"{base}*1 ",
48+
"${spring.expression}",
49+
"#{systemProperties}",
50+
"some[array]element",
51+
"multiplication*sign"
52+
})
53+
void testIsValidReturnsFalseForBackendExpressions(String expressionInput) {
54+
assertFalse(xssValidator.isValid(expressionInput, context));
55+
}
56+
57+
@ParameterizedTest
58+
@ValueSource(strings = {
59+
"<script>alert(1)</script>",
60+
"<html><body>malicious</body></html>",
61+
"<div>plain div tag</div>",
62+
"<b>bold text</b>",
63+
"<img src=x onerror=alert(1)>"
64+
})
65+
void testIsValidReturnsFalseForFrontendXssAndHtmlTags(String htmlInput) {
66+
assertFalse(xssValidator.isValid(htmlInput, context));
67+
}
68+
69+
@ParameterizedTest
70+
@ValueSource(strings = {
71+
"%3cbody%20onresize%3d%22print()%22%3e", // URL encoded <body onresize="print()">
72+
"%3cscript%3ealert(1)%3c/script%3e", // URL encoded <script>alert(1)</script>
73+
"hello%20world%2a" // URL encoded '*' character at the end
74+
})
75+
void testIsValidReturnsFalseForUrlEncodedPayloads(String encodedInput) {
76+
assertFalse(xssValidator.isValid(encodedInput, context));
77+
}
78+
}

0 commit comments

Comments
 (0)