11using Bit . Core . Platform . Mail . Mailer ;
2+ using Bit . Core . Settings ;
23using Bit . Core . Test . Platform . Mailer . TestMail ;
4+ using Microsoft . Extensions . Logging ;
5+ using NSubstitute ;
36using Xunit ;
47
58namespace Bit . Core . Test . Platform . Mailer ;
@@ -9,12 +12,161 @@ public class HandlebarMailRendererTests
912 [ Fact ]
1013 public async Task RenderAsync_ReturnsExpectedHtmlAndTxt ( )
1114 {
12- var renderer = new HandlebarMailRenderer ( ) ;
15+ var logger = Substitute . For < ILogger < HandlebarMailRenderer > > ( ) ;
16+ var globalSettings = new GlobalSettings { SelfHosted = false } ;
17+ var renderer = new HandlebarMailRenderer ( logger , globalSettings ) ;
18+
1319 var view = new TestMailView { Name = "John Smith" } ;
1420
1521 var ( html , txt ) = await renderer . RenderAsync ( view ) ;
1622
1723 Assert . Equal ( "Hello <b>John Smith</b>" , html . Trim ( ) ) ;
1824 Assert . Equal ( "Hello John Smith" , txt . Trim ( ) ) ;
1925 }
26+
27+ [ Fact ]
28+ public async Task RenderAsync_LoadsFromDisk_WhenSelfHostedAndFileExists ( )
29+ {
30+ var logger = Substitute . For < ILogger < HandlebarMailRenderer > > ( ) ;
31+ var tempDir = Path . Combine ( Path . GetTempPath ( ) , Guid . NewGuid ( ) . ToString ( ) ) ;
32+ Directory . CreateDirectory ( tempDir ) ;
33+
34+ try
35+ {
36+ var globalSettings = new GlobalSettings
37+ {
38+ SelfHosted = true ,
39+ MailTemplateDirectory = tempDir
40+ } ;
41+
42+ // Create test template files on disk
43+ var htmlTemplatePath = Path . Combine ( tempDir , "Bit.Core.Test.Platform.Mailer.TestMail.TestMailView.html.hbs" ) ;
44+ var txtTemplatePath = Path . Combine ( tempDir , "Bit.Core.Test.Platform.Mailer.TestMail.TestMailView.text.hbs" ) ;
45+ await File . WriteAllTextAsync ( htmlTemplatePath , "Custom HTML: <b>{{Name}}</b>" ) ;
46+ await File . WriteAllTextAsync ( txtTemplatePath , "Custom TXT: {{Name}}" ) ;
47+
48+ var renderer = new HandlebarMailRenderer ( logger , globalSettings ) ;
49+ var view = new TestMailView { Name = "Jane Doe" } ;
50+
51+ var ( html , txt ) = await renderer . RenderAsync ( view ) ;
52+
53+ Assert . Equal ( "Custom HTML: <b>Jane Doe</b>" , html . Trim ( ) ) ;
54+ Assert . Equal ( "Custom TXT: Jane Doe" , txt . Trim ( ) ) ;
55+ }
56+ finally
57+ {
58+ // Cleanup
59+ if ( Directory . Exists ( tempDir ) )
60+ {
61+ Directory . Delete ( tempDir , true ) ;
62+ }
63+ }
64+ }
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+ }
20172}
0 commit comments