@@ -10,6 +10,7 @@ import org.junit.Before
1010import org.junit.Test
1111
1212import static org.junit.Assert.assertEquals
13+ import static org.junit.Assert.assertNotNull
1314import static org.junit.Assert.assertTrue
1415
1516// see end-of-file for license information
@@ -521,4 +522,144 @@ class JUnitXmlReporterTest {
521522 File [] allFiles = outputPath. listFiles()
522523 assertTrue (" Should have created at least one file or directory" , allFiles != null && allFiles. length > 0 )
523524 }
525+
526+ @Test
527+ void testPathTraversalAttackIsBlocked () {
528+ // Given: a malicious path trying to escape the output directory
529+ // This simulates a path traversal attack like "../../../etc/passwd"
530+ SinglePageResults maliciousPage = new SinglePageResults (
531+ " index.html" ,
532+ " ../../../malicious/path/index.html" ,
533+ " Malicious Page" ,
534+ 1000 ,
535+ new ArrayList<> ())
536+ PerRunResults runResults = new PerRunResults ()
537+ runResults. addPageResults(maliciousPage)
538+
539+ // When: we generate the report in HIERARCHICAL mode
540+ new JUnitXmlReporter (runResults, outputPath. absolutePath, Configuration.JunitOutputStyle . HIERARCHICAL )
541+ .reportPageSummary(maliciousPage)
542+
543+ // Then: the file should be created safely within outputPath, not outside it
544+ File [] allFiles = outputPath. listFiles()
545+ assertTrue (" Should have created file or directory" , allFiles != null && allFiles. length > 0 )
546+
547+ // Verify no files were created outside outputPath
548+ def outputPathCanonical = outputPath. canonicalPath
549+ def createdFile = findFirstXmlFile(outputPath)
550+ assertNotNull (createdFile)
551+
552+ // The created file should be within outputPath
553+ assertTrue (" File should be within output directory" ,
554+ createdFile. canonicalPath. startsWith(outputPathCanonical))
555+ }
556+
557+ @Test
558+ void testPathTraversalWithSymlinkStyleAttackIsBlocked () {
559+ // Given: a more sophisticated path traversal attack that tries to bypass simple checks
560+ // Example: "validdir/../../escape/test.html" which could bypass startsWith() on strings
561+ SinglePageResults sophisticatedAttack = new SinglePageResults (
562+ " test.html" ,
563+ " valid/../../../escape/test.html" ,
564+ " Sophisticated Attack" ,
565+ 1000 ,
566+ new ArrayList<> ())
567+ PerRunResults runResults = new PerRunResults ()
568+ runResults. addPageResults(sophisticatedAttack)
569+
570+ // When: we generate the report in HIERARCHICAL mode
571+ new JUnitXmlReporter (runResults, outputPath. absolutePath, Configuration.JunitOutputStyle . HIERARCHICAL )
572+ .reportPageSummary(sophisticatedAttack)
573+
574+ // Then: verify the file is safely contained
575+ def outputPathCanonical = outputPath. canonicalPath
576+ def createdFile = findFirstXmlFile(outputPath)
577+ assertNotNull (createdFile)
578+
579+ // Use NIO Path API to verify containment (same method as production code)
580+ def normalizedOutputPath = outputPath. canonicalFile. toPath(). normalize()
581+ def normalizedCreatedPath = createdFile. canonicalFile. toPath(). normalize()
582+
583+ assertTrue (" File should be within output directory using NIO Path API" ,
584+ normalizedCreatedPath. startsWith(normalizedOutputPath))
585+ }
586+
587+ @Test
588+ void testEnhancedErrorMessageWhenDirectoryCreationFails () {
589+ // Given: a non-existent parent directory that cannot be created
590+ // We'll use a path that's invalid on the filesystem
591+ File invalidOutputPath = new File (" /nonexistent/deeply/nested/path/that/cannot/be/created" )
592+
593+ SinglePageResults page = new SinglePageResults (
594+ " test.html" ,
595+ " some/deep/path/test.html" ,
596+ " Test Page" ,
597+ 1000 ,
598+ new ArrayList<> ())
599+ PerRunResults runResults = new PerRunResults ()
600+ runResults. addPageResults(page)
601+
602+ // When/Then: directory creation should fail with enhanced error message
603+ try {
604+ new JUnitXmlReporter (runResults, invalidOutputPath. absolutePath, Configuration.JunitOutputStyle . HIERARCHICAL )
605+ .reportPageSummary(page)
606+ fail (" Should have thrown RuntimeException for directory creation failure" )
607+ } catch (RuntimeException e) {
608+ // Verify the error message contains diagnostic information
609+ String errorMsg = e. message
610+ assertTrue (" Error message should mention 'Cannot create directory'" ,
611+ errorMsg. contains(" Cannot create directory" ))
612+ assertTrue (" Error message should contain full path" ,
613+ errorMsg. contains(invalidOutputPath. absolutePath))
614+ assertTrue (" Error message should include 'exists:' diagnostic" ,
615+ errorMsg. contains(" exists:" ))
616+ assertTrue (" Error message should include 'parent canWrite:' diagnostic" ,
617+ errorMsg. contains(" parent canWrite:" ))
618+ }
619+ }
620+
621+ @Test
622+ void testEnhancedErrorMessageFormatIsCorrect () {
623+ // Given: setup that will trigger directory creation failure
624+ File readOnlyParent = new File (outputPath, " readonly-parent" )
625+ readOnlyParent. mkdirs()
626+
627+ // Try to make it read-only (this may not work on all platforms, especially Windows)
628+ boolean madeReadOnly = readOnlyParent. setReadOnly()
629+
630+ if (! madeReadOnly || readOnlyParent. canWrite()) {
631+ // Skip test if we cannot make directory read-only on this platform
632+ System . err. println (" Skipping testEnhancedErrorMessageFormatIsCorrect - cannot make directory read-only on this platform" )
633+ return
634+ }
635+
636+ try {
637+ SinglePageResults page = new SinglePageResults (
638+ " test.html" ,
639+ " readonly-parent/subdir/test.html" ,
640+ " Test Page" ,
641+ 1000 ,
642+ new ArrayList<> ())
643+ PerRunResults runResults = new PerRunResults ()
644+ runResults. addPageResults(page)
645+
646+ // When: attempting to create subdirectory in read-only parent
647+ new JUnitXmlReporter (runResults, outputPath. absolutePath, Configuration.JunitOutputStyle . HIERARCHICAL )
648+ .reportPageSummary(page)
649+ fail (" Should have thrown RuntimeException" )
650+ } catch (RuntimeException e) {
651+ // Then: error message should have proper format with parentheses
652+ String errorMsg = e. message
653+ assertTrue (" Error message should contain opening parenthesis" ,
654+ errorMsg. contains(" (" ))
655+ assertTrue (" Error message should contain closing parenthesis" ,
656+ errorMsg. contains(" )" ))
657+ // Should have format like: "... (exists: true, parent canWrite: false)"
658+ assertTrue (" Error message should match expected format pattern" ,
659+ errorMsg. matches(" .*\\ (exists: .*, parent canWrite: .*\\ ).*" ))
660+ } finally {
661+ // Cleanup: restore write permission
662+ readOnlyParent. setWritable(true )
663+ }
664+ }
524665}
0 commit comments