@@ -166,6 +166,75 @@ func TestStaticFileNestedSymlinkEscape(t *testing.T) {
166166 }
167167}
168168
169+ // TestStaticFileSymlinkWorkspace 验证工作空间父目录符号链接不影响资源访问及敏感路径拦截。
170+ func TestStaticFileSymlinkWorkspace (t * testing.T ) {
171+ gin .SetMode (gin .TestMode )
172+ realHome , err := filepath .EvalSymlinks (t .TempDir ())
173+ if err != nil {
174+ t .Fatal (err )
175+ }
176+ linkedHome := filepath .Join (t .TempDir (), "home" )
177+ if err = os .Symlink (realHome , linkedHome ); err != nil {
178+ t .Skipf ("create directory symlink failed: %s" , err )
179+ }
180+ originalWorkspace := util .WorkspaceDir
181+ util .WorkspaceDir = filepath .Join (linkedHome , ".var" , "app" , "org.b3log.siyuan" , "SiYuan" )
182+ t .Cleanup (func () { util .WorkspaceDir = originalWorkspace })
183+ for _ , rel := range []string {
184+ "data/emojis/fontawesome-free-solid/robot.svg" ,
185+ "data/widgets/listChildDocs/index.html" ,
186+ "data/widgets/listChildDocs/app.js" ,
187+ "data/widgets/listChildDocs/credentials.json" ,
188+ "conf/conf.json" ,
189+ "temp/private.txt" ,
190+ } {
191+ p := filepath .Join (util .WorkspaceDir , filepath .FromSlash (rel ))
192+ if err = os .MkdirAll (filepath .Dir (p ), 0755 ); err != nil {
193+ t .Fatal (err )
194+ }
195+ if err = os .WriteFile (p , []byte ("content" ), 0644 ); err != nil {
196+ t .Fatal (err )
197+ }
198+ }
199+ widgets := filepath .Join (util .WorkspaceDir , "data" , "widgets" )
200+ for _ , dir := range []string {"conf" , "temp" } {
201+ if err = os .Symlink (filepath .Join (util .WorkspaceDir , dir ), filepath .Join (widgets , dir )); err != nil {
202+ t .Fatal (err )
203+ }
204+ }
205+ if err = os .Symlink (filepath .Join (util .WorkspaceDir , "conf" ), filepath .Join (widgets , "listChildDocs" , "escape" )); err != nil {
206+ t .Fatal (err )
207+ }
208+ engine := gin .New ()
209+ registerStaticFileHandlers (engine .Group ("/emojis" ), filepath .Join (util .WorkspaceDir , "data" , "emojis" ), false , nil )
210+ registerStaticFileHandlers (engine .Group ("/widgets" ), widgets , true , nil )
211+ for _ , test := range []struct {
212+ path string
213+ status int
214+ }{
215+ {"/emojis/fontawesome-free-solid/robot.svg" , http .StatusOK },
216+ {"/widgets/listChildDocs/" , http .StatusOK },
217+ {"/widgets/listChildDocs/app.js" , http .StatusOK },
218+ {"/widgets/listChildDocs/credentials.json" , http .StatusForbidden },
219+ {"/widgets/conf/conf.json" , http .StatusForbidden },
220+ {"/widgets/temp/private.txt" , http .StatusForbidden },
221+ {"/widgets/listChildDocs/escape/conf.json" , http .StatusForbidden },
222+ } {
223+ for _ , method := range []string {http .MethodGet , http .MethodHead } {
224+ t .Run (method + " " + test .path , func (t * testing.T ) {
225+ recorder := httptest .NewRecorder ()
226+ engine .ServeHTTP (recorder , httptest .NewRequest (method , test .path , nil ))
227+ if recorder .Code != test .status {
228+ t .Fatalf ("status = %d, want %d" , recorder .Code , test .status )
229+ }
230+ if method == http .MethodGet && test .status == http .StatusOK && recorder .Body .String () != "content" {
231+ t .Fatalf ("unexpected response body: %q" , recorder .Body .String ())
232+ }
233+ })
234+ }
235+ }
236+ }
237+
169238func TestWidgetResponseCacheControl (t * testing.T ) {
170239 gin .SetMode (gin .TestMode )
171240 originalDataDir , originalConf := util .DataDir , model .Conf
0 commit comments