Skip to content

Commit bba046e

Browse files
jeremiahjstaceykwwall
authored andcommitted
Date validation rule 299 (ESAPI#468)
* DateValidationRule Logic Updates Updating the parsing logic of the DateValidationRule to not only verify a date can be constructed from the input string in a known format, but also that the generated Date can be reformatted back to the same String content. This adds additional security for cases where the DateFormatter accepts characters postfixed to an otherwise valid date. As a result two tests in ValidationTest.java had to be updated to use the correct explicit format. The data sets were using full-name month, day, year content, but supplying the MEDIUM format. Updating the provided format to LONG allowed the new check to function as desired. * DateValidationRule Test Content Adding test cases presented in the github issue * Dependency Cleanup: JodaTime Removing JodaTime dependency from Pom. It had been introduced to potentially address date/time parsing issues, but had not yet been applied to the baseline. * DateValidationRule Test Content Updating test to actually use assertions in execution so that the intended state is verified. * DateValidationRule Test Content Adding additional test case content. * Validation Test Cleanup Converting ValidationErrorList Test to use Junit4 syntax. * Validation Test Cleanup Updating assertions used in ValidationErrorList for more information in Junit test failure cases. * Validation Test Cleanup Breaking the exception cases of the ValidationErrorListTest into separate test blocks. Updated to use the ExpectedException Rule. Corrected wording in implementation error message on null context. Corrected message in implementation on null ValidationException reference. * Validation Test Cleanup Test updates to use class-scope references rather than on a per-test basis. Removed method to create ValidationException. Catch block indicated that a specific exception had been thrown at one point. I checked the call heirarchy and verified that exception is not being explicitly thrown at this time. * Validation Test Cleanup ValidationErrorListTest: Removed sysouts Cleaned unused imports Formatted file (removed tab chars & adjusted indent) * Validation Test Cleanup BaseValidationRuleTest Converting test to Junit4 Syntax. * Validation Test Cleanup: BaseValidationRuleTest Commenting out the current test. Upon inspection, the test is not fulfilling the commented intent. Instead of validating that the BaseValidationRule throws exceptions, it is only verifying that the test-scope stub implementation throws. Going to keep it around so I can verify I'm fulfilling the intent in the new implementation content before deleting that code * Validation Test Cleanup: BaseValidationRuleTest Re-implementing the tests for BaseValidationRule. ** This commit contains a failing test (testWhitelistSetExtendedCharacterSets)** I do not know how to test utf-16 character sets, but I think it is something that needs to be done. I have listed a concern with potential side-effects of using ValidationErrorList in the BaseValidationRule. Refer to testGetValidMultipleExceptionSameContextThrowsRuntimeException for more information. * Validation Test Cleanup: DateValidationRuleTest Initial basic tests for general class functionality. * Validation Test Cleanup: DateValidationRuleTest Adding validations to ensure that DateFormat leniency is being set in accordance with the SecurityConfiguration ACCEPT_LENIENT_DATES value. Still light in the context that I'm not verifying the value is derived from the SecurityConfiguration, but I am at least checking that the date format is being updated and that the value being applied matches the test-scope settings. * DateValidationRule Logic & Test Updates Updating the implementation so the process of generating a String from the parsed date and comparing it with the canonicalized input only happens during the sanitize workflow. This will allow for the DateFormat contract for leniency and ESAPI's contract for security to remain intact (I think). In essence, getValid is asking if the configured DateFormat can parse any date out of String being provided. That's completely on the DateFormat instance. If it works then return it, if it doesn't the throw the ValidationException. On the other hand, sanitize is asking for a safe representation of the String. For that we can add the additional cross check and be more stringent in the criteria. Meaning if the same String that was used to generate the Date cannot be recreated from that date then we don't trust the input. Tests were updated to reflect this workflow, and the data set used to check 299 in the ValidatorTest has been integrated into DateValidationRuleTest. * Validation Test Updates: BaseValidationRuleTest Ignoring the explictly fail case in the test. * DateValidation Corrections for DefaultValidator Adding another sanitize method to the DateValidationRule which accepts the ValidationErrorList in addition to the other parameters. Updating DefaultValidator to use the new sanitize method and check the error list to verify the returned date before passing it along to the client. If there are no errors, we're gtg; otherwise, the ValidationError at the first index is rethrown. * DateValidationTest API Extension Adding tests for new sanitize API method. * DefaultValidator Date Validation Logic Update Altering method chaining to defer to the getValidDate which uses the ValidationErrorList which now contains the delegation to the DateValidationRule. * DefaultValidator Date API Testing Creating a new test class that focuses on testing the DATE API of DefaultValidator. This is a wireframe commit. * DefaultValidator Date API Testing Successful interception of the DateValidatorRule construction. When DefaultValidator instantiates a new instance internally, it receives the test-scope spy. This will allow us to check that the intended workflow of how the DefaultValidator delegates to the DateValidatorRule is operating as expected without needing to re-test the conditions already verified in the DateValidatorRuleTest. * DefaultValidator Logic Update & Date API Tests Working tests to verify happy-path workflow through DefaultValidator Date API. Logic update to isValidDate with ValidationErrorList to use the ValidationErrorList that is passed in with the delegated DateValidationRule.sanitize call. * DefaultValidator Date API Negative Tests Verifying workflow and response from the DateAPI when the DateValidationRule would result in a ValidationException being thrown. * DefaultValidator Date API Test Cleanup No functional changes. Mostly updating static imports and references. * DefaultValidator cleanup Fixing inconsistent indentation in updated method block. * ValidatorTest Cleanup :: New ESAPI API Test Created a new test for the static ESAPI class which verifies the workflow by which the runtime Validator is derived. Removed the date validity tests from the ValidatorTest. All pertinent content has been divided into the BaseValidationRuleTest, DateValidationRuleTest, ValidationErrorListTest, DefaultValidatorDateAPITest, and ESAPIContractAPITests. The specific conditions have been shifted to be exclusively against the implementation (BaseValidationRuleTest). It is shown that the DefaultValidator uses BaseValidationRule in all date-related checks (DefaultValidatorDateAPITest). And we know that if the user configures the ESAPI validator property to the DefaultValidator it will resolve at runtime (ESAPIContractAPITests). * DateValdationRuleTest Additions Adding Powermock tests that show the lenient setting is derived from the SecurityConfiguration and is applied to the DateFormat reference both when a DateValidationRule is constructed, and when the setDateFormat method is invoked. I chose to implement this as a unique test since PowermockRunner impacts my ability to see code coverage. * BaseValidationRuleTest UTF-16 Whitelist Addition Replacing ignore/fail of test impl with content showing that extended character sets can be whitelisted through the implementation.
1 parent 48cb2ad commit bba046e

File tree

11 files changed

+1044
-295
lines changed

11 files changed

+1044
-295
lines changed

pom.xml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,6 @@
129129
</properties>
130130

131131
<dependencies>
132-
<!-- https://mvnrepository.com/artifact/joda-time/joda-time -->
133-
<dependency>
134-
<groupId>joda-time</groupId>
135-
<artifactId>joda-time</artifactId>
136-
<version>2.10</version>
137-
</dependency>
138132
<dependency>
139133
<groupId>commons-configuration</groupId>
140134
<artifactId>commons-configuration</artifactId>

src/main/java/org/owasp/esapi/ValidationErrorList.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ public class ValidationErrorList {
9191
* @param vex A {@code ValidationException}.
9292
*/
9393
public void addError(String context, ValidationException vex) {
94-
if ( context == null ) throw new RuntimeException( "Context for cannot be null: " + vex.getLogMessage(), vex );
95-
if ( vex == null ) throw new RuntimeException( "Context (" + context + ") cannot be null" );
94+
if ( context == null ) throw new RuntimeException( "Context cannot be null: " + vex.getLogMessage(), vex );
95+
if ( vex == null ) throw new RuntimeException( "ValidationException cannot be null for context (" + context + ")" );
9696
if (getError(context) != null) throw new RuntimeException("Context (" + context + ") already exists, must be unique");
9797
errorList.put(context, vex);
9898
}

src/main/java/org/owasp/esapi/reference/DefaultValidator.java

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -277,35 +277,38 @@ public boolean isValidDate(String context, String input, DateFormat format, bool
277277
* {@inheritDoc}
278278
*/
279279
public boolean isValidDate(String context, String input, DateFormat format, boolean allowNull, ValidationErrorList errors) throws IntrusionException {
280-
try {
281-
getValidDate( context, input, format, allowNull);
282-
return true;
283-
} catch( ValidationException e ) {
284-
errors.addError(context, e);
285-
return false;
286-
}
280+
getValidDate( context, input, format, allowNull, errors);
281+
return errors.isEmpty();
287282
}
288283

289284
/**
290285
* {@inheritDoc}
291286
*/
292287
public Date getValidDate(String context, String input, DateFormat format, boolean allowNull) throws ValidationException, IntrusionException {
293-
DateValidationRule dvr = new DateValidationRule( "SimpleDate", encoder, format);
294-
dvr.setAllowNull(allowNull);
295-
return dvr.getValid(context, input);
288+
289+
ValidationErrorList vel = new ValidationErrorList();
290+
Date validDate = getValidDate(context, input, format, allowNull, vel);
291+
292+
if (vel.isEmpty()) {
293+
return validDate;
294+
}
295+
296+
throw vel.errors().get(0);
296297
}
297298

298299
/**
299300
* {@inheritDoc}
300301
*/
301302
public Date getValidDate(String context, String input, DateFormat format, boolean allowNull, ValidationErrorList errors) throws IntrusionException {
302-
try {
303-
return getValidDate(context, input, format, allowNull);
304-
} catch (ValidationException e) {
305-
errors.addError(context, e);
306-
}
307-
// error has been added to list, so return null
308-
return null;
303+
Date safeDate = null;
304+
DateValidationRule dvr = new DateValidationRule( "SimpleDate", encoder, format);
305+
dvr.setAllowNull(allowNull);
306+
safeDate = dvr.sanitize(context, input, errors);
307+
if (!errors.isEmpty()) {
308+
safeDate = null;
309+
}
310+
// error has been added to list, so return null
311+
return safeDate;
309312
}
310313

311314
/**

src/main/java/org/owasp/esapi/reference/validation/DateValidationRule.java

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.owasp.esapi.ESAPI;
2222
import org.owasp.esapi.Encoder;
2323
import org.owasp.esapi.StringUtilities;
24+
import org.owasp.esapi.ValidationErrorList;
2425
import org.owasp.esapi.errors.ValidationException;
2526

2627
/**
@@ -60,7 +61,7 @@ public final void setDateFormat( DateFormat newFormat ) {
6061
* {@inheritDoc}
6162
*/
6263
public Date getValid( String context, String input ) throws ValidationException {
63-
return safelyParse(context, input);
64+
return safelyParse(context, input, false);
6465
}
6566

6667
/**
@@ -70,16 +71,25 @@ public Date getValid( String context, String input ) throws ValidationException
7071
*/
7172
@Override
7273
public Date sanitize( String context, String input ) {
73-
Date date = new Date(0);
74-
try {
75-
date = safelyParse(context, input);
76-
} catch (ValidationException e) {
77-
// do nothing
78-
}
79-
return date;
74+
return sanitize(context, input, null);
8075
}
76+
77+
/**
78+
* {@inheritDoc}
79+
*/
80+
public Date sanitize( String context, String input, ValidationErrorList errorList ) {
81+
Date date = new Date(0);
82+
try {
83+
date = safelyParse(context, input, true);
84+
} catch (ValidationException e) {
85+
if (errorList != null) {
86+
errorList.addError(context, e);
87+
}
88+
}
89+
return date;
90+
}
8191

82-
private Date safelyParse(String context, String input)
92+
private Date safelyParse(String context, String input, boolean sanitize)
8393
throws ValidationException {
8494
// CHECKME should this allow empty Strings? " " use IsBlank instead?
8595
if (StringUtilities.isEmpty(input)) {
@@ -91,10 +101,16 @@ private Date safelyParse(String context, String input)
91101
+ input, context);
92102
}
93103

94-
String canonical = encoder.canonicalize(input);
95-
96-
try {
97-
return format.parse(canonical);
104+
String canonical = encoder.canonicalize(input);
105+
try {
106+
Date rval = format.parse(canonical);
107+
if (sanitize) {
108+
String cycled = format.format(rval);
109+
if (!cycled.equals(canonical)) {
110+
throw new Exception("Parameter date is not a clean translation between String and Date contexts. Check input for additional characters");
111+
}
112+
}
113+
return rval;
98114
} catch (Exception e) {
99115
throw new ValidationException(context
100116
+ ": Invalid date must follow the "
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package org.owasp.esapi;
2+
3+
import org.junit.Assert;
4+
import org.junit.Before;
5+
import org.junit.Test;
6+
import org.junit.runner.RunWith;
7+
import org.mockito.ArgumentMatchers;
8+
import org.mockito.Mock;
9+
import org.mockito.Mockito;
10+
import org.mockito.internal.verification.VerificationModeFactory;
11+
import org.owasp.esapi.util.ObjFactory;
12+
import org.powermock.api.mockito.PowerMockito;
13+
import org.powermock.core.classloader.annotations.PrepareForTest;
14+
import org.powermock.modules.junit4.PowerMockRunner;
15+
16+
17+
@RunWith(PowerMockRunner.class)
18+
@PrepareForTest({ObjFactory.class})
19+
public class ESAPIContractAPITest {
20+
21+
@Mock
22+
private SecurityConfiguration mockSecConfig;
23+
24+
@Mock
25+
private Validator mockValidator;
26+
27+
@Before
28+
public void configureStaticContexts() throws Exception {
29+
PowerMockito.mockStatic(ObjFactory.class);
30+
PowerMockito.when(ObjFactory.class, "make", ArgumentMatchers.anyString(), ArgumentMatchers.eq("SecurityConfiguration")).thenReturn(mockSecConfig);
31+
PowerMockito.when(ObjFactory.class, "make", ArgumentMatchers.eq("MOCK_TEST_VALIDATOR"), ArgumentMatchers.eq("Validator")).thenReturn(mockValidator);
32+
33+
PowerMockito.when(mockSecConfig.getValidationImplementation()).thenReturn("MOCK_TEST_VALIDATOR");
34+
}
35+
36+
@Test
37+
public void testValidatorFromConfiguration() {
38+
Validator validator = ESAPI.validator();
39+
Assert.assertEquals("ESAPI Configuration should return Validator as specified by the SecurityConfiguration", mockValidator, validator);
40+
41+
PowerMockito.verifyStatic(ObjFactory.class, VerificationModeFactory.times(1));
42+
ObjFactory.make(ArgumentMatchers.anyString(), ArgumentMatchers.eq("SecurityConfiguration"));
43+
44+
PowerMockito.verifyStatic(ObjFactory.class, VerificationModeFactory.times(1));
45+
ObjFactory.make(ArgumentMatchers.eq("MOCK_TEST_VALIDATOR"), ArgumentMatchers.eq("Validator"));
46+
47+
PowerMockito.verifyNoMoreInteractions(ObjFactory.class);
48+
49+
Mockito.verify(mockSecConfig, Mockito.times(1)).getValidationImplementation();
50+
}
51+
52+
}

src/test/java/org/owasp/esapi/ValidationErrorListTest.java

Lines changed: 57 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -15,126 +15,76 @@
1515
*/
1616
package org.owasp.esapi;
1717

18-
import junit.framework.Test;
19-
import junit.framework.TestCase;
20-
import junit.framework.TestSuite;
18+
import static org.junit.Assert.assertEquals;
19+
import static org.junit.Assert.assertFalse;
20+
import static org.junit.Assert.assertNull;
21+
import static org.junit.Assert.assertTrue;
2122

22-
import org.owasp.esapi.errors.IntrusionException;
23+
import org.junit.Rule;
24+
import org.junit.Test;
25+
import org.junit.rules.ExpectedException;
26+
import org.junit.rules.TestName;
2327
import org.owasp.esapi.errors.ValidationException;
2428

29+
2530
/**
2631
* @author Jeff Williams ([email protected])
2732
*/
28-
public class ValidationErrorListTest extends TestCase {
29-
30-
/**
31-
* Instantiates a new executor test.
32-
*
33-
* @param testName
34-
* the test name
35-
*/
36-
public ValidationErrorListTest(String testName) {
37-
super(testName);
38-
}
39-
40-
/**
41-
* {@inheritDoc}
42-
*
43-
* @throws Exception
44-
*/
45-
protected void setUp() throws Exception {
46-
// none
47-
}
33+
public class ValidationErrorListTest {
34+
@Rule
35+
public ExpectedException exEx = ExpectedException.none();
36+
@Rule
37+
public TestName testName = new TestName();
4838

49-
/**
50-
* {@inheritDoc}
51-
*
52-
* @throws Exception
53-
*/
54-
protected void tearDown() throws Exception {
55-
// none
56-
}
39+
ValidationErrorList vel = new ValidationErrorList();
40+
ValidationException vex = new ValidationException(testName.getMethodName(), testName.getMethodName());
41+
@Test
42+
public void testAddErrorNullContextThrows() {
43+
exEx.expect(RuntimeException.class);
44+
exEx.expectMessage("Context cannot be null");
45+
vel.addError(null, vex);
46+
}
5747

58-
/**
59-
* Suite.
60-
*
61-
* @return the test
62-
*/
63-
public static Test suite() {
64-
TestSuite suite = new TestSuite(ValidationErrorListTest.class);
65-
return suite;
66-
}
67-
68-
public void testAddError() throws Exception {
69-
System.out.println("testAddError");
70-
ValidationErrorList vel = new ValidationErrorList();
71-
ValidationException vex = createValidationException();
72-
vel.addError("context", vex );
73-
try {
74-
vel.addError(null, vex );
75-
fail();
76-
} catch( Exception e ) {
77-
// expected
78-
}
79-
try {
80-
vel.addError("context1", null );
81-
fail();
82-
} catch( Exception e ) {
83-
// expected
84-
}
85-
try {
86-
vel.addError("context", vex ); // add the same context again
87-
fail();
88-
} catch( Exception e ) {
89-
// expected
90-
}
91-
}
48+
@Test
49+
public void testAddErrorNullExceptionThrows() {
50+
exEx.expect(RuntimeException.class);
51+
exEx.expectMessage("ValidationException cannot be null");
52+
vel.addError(testName.getMethodName(), null);
53+
}
54+
public void testAddErrorDuplicateContextThrows() {
55+
exEx.expect(RuntimeException.class);
56+
exEx.expectMessage("already exists, must be unique");
57+
vel.addError(testName.getMethodName(), vex);
58+
vel.addError(testName.getMethodName(), vex);
59+
}
9260

93-
public void testErrors() throws Exception {
94-
System.out.println("testErrors");
95-
ValidationErrorList vel = new ValidationErrorList();
96-
ValidationException vex = createValidationException();
97-
vel.addError("context", vex );
98-
assertTrue( vel.errors().get(0) == vex );
99-
}
61+
@Test
62+
public void testErrors() throws Exception {
63+
vel.addError("context", vex );
64+
assertTrue("Validation Errors List should contain the added ValidationException Reference",vel.errors().contains( vex) );
65+
}
10066

101-
public void testGetError() throws Exception {
102-
System.out.println("testGetError");
103-
ValidationErrorList vel = new ValidationErrorList();
104-
ValidationException vex = createValidationException();
105-
vel.addError("context", vex );
106-
assertTrue( vel.getError( "context" ) == vex );
107-
assertTrue( vel.getError( "ridiculous" ) == null );
108-
}
67+
@Test
68+
public void testGetError() throws Exception {
69+
vel.addError("context", vex );
70+
assertTrue( vel.getError( "context" ) == vex );
71+
assertNull( vel.getError( "ridiculous" ) );
72+
}
10973

110-
public void testIsEmpty() throws Exception {
111-
System.out.println("testIsEmpty");
112-
ValidationErrorList vel = new ValidationErrorList();
113-
assertTrue( vel.isEmpty() );
114-
ValidationException vex = createValidationException();
115-
vel.addError("context", vex );
116-
assertFalse( vel.isEmpty() );
117-
}
74+
@Test
75+
public void testIsEmpty() throws Exception {
76+
assertTrue( vel.isEmpty() );
77+
vel.addError("context", vex );
78+
assertFalse( vel.isEmpty() );
79+
}
11880

119-
public void testSize() throws Exception {
120-
System.out.println("testSize");
121-
ValidationErrorList vel = new ValidationErrorList();
122-
assertTrue( vel.size() == 0 );
123-
ValidationException vex = createValidationException();
124-
vel.addError("context", vex );
125-
assertTrue( vel.size() == 1 );
126-
}
81+
@Test
82+
public void testSize() throws Exception {
83+
assertEquals(0, vel.size() );
84+
vel.addError("context", vex );
85+
assertEquals(1, vel.size());
86+
}
12787

128-
private ValidationException createValidationException() {
129-
ValidationException vex = null;
130-
try {
131-
vex = new ValidationException("User message", "Log Message");
132-
} catch( IntrusionException e ) {
133-
// expected occasionally
134-
}
135-
return vex;
136-
}
137-
13888
}
13989

14090

0 commit comments

Comments
 (0)