Skip to content

Commit 3d61c35

Browse files
committed
Merge branch 'release/0.9.10'
2 parents fbcde11 + 05da8d9 commit 3d61c35

File tree

4 files changed

+60
-26
lines changed

4 files changed

+60
-26
lines changed

.version.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
"strategy": "semver",
33
"major": 0,
44
"minor": 9,
5-
"patch": 9,
5+
"patch": 10,
66
"build": 0
77
}

src/Mvc/Views/Markdown.php

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,11 @@ public function render( array $data ): string
8080
}
8181

8282
/**
83-
* Find markdown file in controller directory or nested subdirectories
83+
* Find markdown file using directory path
8484
*
85-
* @param string $basePath
86-
* @param string $pageName
87-
* @return string|null
85+
* @param string $basePath Base directory for controller views
86+
* @param string $pageName Relative path to markdown file (e.g., "cms/guides/authentication")
87+
* @return string|null Full path to markdown file or null if not found
8888
*/
8989
protected function findMarkdownFile( string $basePath, string $pageName ): ?string
9090
{
@@ -93,29 +93,22 @@ protected function findMarkdownFile( string $basePath, string $pageName ): ?stri
9393
return null;
9494
}
9595

96-
// First check direct path
97-
$directPath = "$basePath/$pageName.md";
98-
if( file_exists( $directPath ) )
96+
// Normalize path separators to forward slashes
97+
$pageName = str_replace( '\\', '/', $pageName );
98+
99+
// Security: prevent directory traversal attacks
100+
if( str_contains( $pageName, '..' ) )
99101
{
100-
return $directPath;
102+
return null;
101103
}
102104

103-
// Search recursively in subdirectories
104-
$iterator = new \RecursiveIteratorIterator(
105-
new \RecursiveDirectoryIterator( $basePath, \RecursiveDirectoryIterator::SKIP_DOTS ),
106-
\RecursiveIteratorIterator::SELF_FIRST
107-
);
105+
// Build full path
106+
$fullPath = "$basePath/$pageName.md";
108107

109-
foreach( $iterator as $file )
108+
// Return path if file exists
109+
if( file_exists( $fullPath ) )
110110
{
111-
if( $file->isFile() && $file->getExtension() === 'md' )
112-
{
113-
$fileName = $file->getBasename( '.md' );
114-
if( $fileName === $pageName )
115-
{
116-
return $file->getPathname();
117-
}
118-
}
111+
return $fullPath;
119112
}
120113

121114
return null;

tests/Mvc/Views/MarkdownNestedTest.php

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public function testFindMarkdownFileInSubfolder()
6161
$method->setAccessible( true );
6262

6363
$basePath = vfsStream::url( 'views/testcontroller' );
64-
$result = $method->invoke( $this->markdown, $basePath, 'page2' );
64+
$result = $method->invoke( $this->markdown, $basePath, 'subfolder/page2' );
6565

6666
$this->assertNotNull( $result );
6767
$this->assertStringContainsString( 'subfolder', $result );
@@ -75,7 +75,7 @@ public function testFindMarkdownFileInDeepNestedFolder()
7575
$method->setAccessible( true );
7676

7777
$basePath = vfsStream::url( 'views/testcontroller' );
78-
$result = $method->invoke( $this->markdown, $basePath, 'page3' );
78+
$result = $method->invoke( $this->markdown, $basePath, 'subfolder/deep/page3' );
7979

8080
$this->assertNotNull( $result );
8181
$this->assertStringContainsString( 'deep', $result );
@@ -107,13 +107,51 @@ public function testFindMarkdownFileReturnsNullForInvalidBasePath()
107107

108108
public function testRenderWithNestedMarkdownFile()
109109
{
110-
$this->markdown->setPage( 'page2' );
110+
$this->markdown->setPage( 'subfolder/page2' );
111111

112112
$result = $this->markdown->render( [] );
113113

114114
$this->assertStringContainsString( '<h1>Nested Page</h1>', $result );
115115
$this->assertStringContainsString( '<html>', $result );
116116
$this->assertStringContainsString( '</html>', $result );
117117
}
118+
119+
public function testFindMarkdownFileWithBackslashSeparator()
120+
{
121+
$reflection = new \ReflectionClass( $this->markdown );
122+
$method = $reflection->getMethod( 'findMarkdownFile' );
123+
$method->setAccessible( true );
124+
125+
$basePath = vfsStream::url( 'views/testcontroller' );
126+
$result = $method->invoke( $this->markdown, $basePath, 'subfolder\page2' );
127+
128+
$this->assertNotNull( $result );
129+
$this->assertStringContainsString( 'subfolder', $result );
130+
$this->assertStringEndsWith( 'page2.md', $result );
131+
}
132+
133+
public function testFindMarkdownFileBlocksDirectoryTraversal()
134+
{
135+
$reflection = new \ReflectionClass( $this->markdown );
136+
$method = $reflection->getMethod( 'findMarkdownFile' );
137+
$method->setAccessible( true );
138+
139+
$basePath = vfsStream::url( 'views/testcontroller' );
140+
$result = $method->invoke( $this->markdown, $basePath, '../page1' );
141+
142+
$this->assertNull( $result );
143+
}
144+
145+
public function testFindMarkdownFileBlocksComplexDirectoryTraversal()
146+
{
147+
$reflection = new \ReflectionClass( $this->markdown );
148+
$method = $reflection->getMethod( 'findMarkdownFile' );
149+
$method->setAccessible( true );
150+
151+
$basePath = vfsStream::url( 'views/testcontroller' );
152+
$result = $method->invoke( $this->markdown, $basePath, 'subfolder/../../page1' );
153+
154+
$this->assertNull( $result );
155+
}
118156
}
119157

versionlog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## 0.9.10 2025-11-27
2+
* Updated markdown to respect file paths.
3+
14
## 0.9.9 2025-11-27
25
* Added MVC events.
36

0 commit comments

Comments
 (0)