@@ -62,4 +62,111 @@ public async Task RenderAsync_LoadsFromDisk_WhenSelfHostedAndFileExists()
6262 }
6363 }
6464 }
65+
66+ [ Theory ]
67+ [ InlineData ( "../../../etc/passwd" ) ]
68+ [ InlineData ( "../../../../malicious.txt" ) ]
69+ [ InlineData ( "../../malicious.txt" ) ]
70+ [ InlineData ( "../malicious.txt" ) ]
71+ public async Task ReadSourceFromDiskAsync_PrevenetsPathTraversal_WhenMaliciousPathProvided ( string maliciousPath )
72+ {
73+ var logger = Substitute . For < ILogger < HandlebarMailRenderer > > ( ) ;
74+ var tempDir = Path . Combine ( Path . GetTempPath ( ) , Guid . NewGuid ( ) . ToString ( ) ) ;
75+ Directory . CreateDirectory ( tempDir ) ;
76+
77+ try
78+ {
79+ var globalSettings = new GlobalSettings
80+ {
81+ SelfHosted = true ,
82+ MailTemplateDirectory = tempDir
83+ } ;
84+
85+ // Create a malicious file outside the template directory
86+ var maliciousFile = Path . Combine ( Path . GetTempPath ( ) , "malicious.txt" ) ;
87+ await File . WriteAllTextAsync ( maliciousFile , "Malicious Content" ) ;
88+
89+ var renderer = new HandlebarMailRenderer ( logger , globalSettings ) ;
90+
91+ // Use reflection to call the private ReadSourceFromDiskAsync method
92+ var method = typeof ( HandlebarMailRenderer ) . GetMethod ( "ReadSourceFromDiskAsync" ,
93+ System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Instance ) ;
94+ var task = ( Task < string ? > ) method ! . Invoke ( renderer , new object [ ] { maliciousPath } ) ! ;
95+ var result = await task ;
96+
97+ // Should return null and not load the malicious file
98+ Assert . Null ( result ) ;
99+
100+ // Verify that a warning was logged for the path traversal attempt
101+ logger . Received ( 1 ) . Log (
102+ LogLevel . Warning ,
103+ Arg . Any < EventId > ( ) ,
104+ Arg . Any < object > ( ) ,
105+ Arg . Any < Exception > ( ) ,
106+ Arg . Any < Func < object , Exception , string > > ( ) ) ;
107+
108+ // Cleanup malicious file
109+ if ( File . Exists ( maliciousFile ) )
110+ {
111+ File . Delete ( maliciousFile ) ;
112+ }
113+ }
114+ finally
115+ {
116+ // Cleanup
117+ if ( Directory . Exists ( tempDir ) )
118+ {
119+ Directory . Delete ( tempDir , true ) ;
120+ }
121+ }
122+ }
123+
124+ [ Fact ]
125+ public async Task ReadSourceFromDiskAsync_AllowsValidFileWithDifferentCase_WhenCaseInsensitiveFileSystem ( )
126+ {
127+ var logger = Substitute . For < ILogger < HandlebarMailRenderer > > ( ) ;
128+ var tempDir = Path . Combine ( Path . GetTempPath ( ) , Guid . NewGuid ( ) . ToString ( ) ) ;
129+ Directory . CreateDirectory ( tempDir ) ;
130+
131+ try
132+ {
133+ var globalSettings = new GlobalSettings
134+ {
135+ SelfHosted = true ,
136+ MailTemplateDirectory = tempDir
137+ } ;
138+
139+ // Create a test template file
140+ var templateFileName = "TestTemplate.hbs" ;
141+ var templatePath = Path . Combine ( tempDir , templateFileName ) ;
142+ await File . WriteAllTextAsync ( templatePath , "Test Content" ) ;
143+
144+ var renderer = new HandlebarMailRenderer ( logger , globalSettings ) ;
145+
146+ // Try to read with different case (should work on case-insensitive file systems like Windows/macOS)
147+ var method = typeof ( HandlebarMailRenderer ) . GetMethod ( "ReadSourceFromDiskAsync" ,
148+ System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Instance ) ;
149+ var task = ( Task < string ? > ) method ! . Invoke ( renderer , new object [ ] { templateFileName } ) ! ;
150+ var result = await task ;
151+
152+ // Should successfully read the file
153+ Assert . Equal ( "Test Content" , result ) ;
154+
155+ // Verify no warning was logged
156+ logger . DidNotReceive ( ) . Log (
157+ LogLevel . Warning ,
158+ Arg . Any < EventId > ( ) ,
159+ Arg . Any < object > ( ) ,
160+ Arg . Any < Exception > ( ) ,
161+ Arg . Any < Func < object , Exception , string > > ( ) ) ;
162+ }
163+ finally
164+ {
165+ // Cleanup
166+ if ( Directory . Exists ( tempDir ) )
167+ {
168+ Directory . Delete ( tempDir , true ) ;
169+ }
170+ }
171+ }
65172}
0 commit comments