@@ -13,6 +13,7 @@ import (
1313 "time"
1414
1515 "ambient-code-backend/git"
16+ "ambient-code-backend/pathutil"
1617
1718 "github.com/gin-gonic/gin"
1819)
@@ -61,7 +62,7 @@ func ContentGitPush(c *gin.Context) {
6162 }
6263
6364 // Basic safety: repoDir must be under StateBaseDir
64- if ! strings . HasPrefix (repoDir + string ( os . PathSeparator ) , StateBaseDir + string ( os . PathSeparator ) ) && repoDir != StateBaseDir {
65+ if ! pathutil . IsPathWithinBase (repoDir , StateBaseDir ) && repoDir != StateBaseDir {
6566 log .Printf ("contentGitPush: invalid repoPath resolved=%q stateBaseDir=%q" , repoDir , StateBaseDir )
6667 c .JSON (http .StatusBadRequest , gin.H {"error" : "invalid repoPath" })
6768 return
@@ -101,7 +102,7 @@ func ContentGitAbandon(c *gin.Context) {
101102 repoDir = StateBaseDir
102103 }
103104
104- if ! strings . HasPrefix (repoDir + string ( os . PathSeparator ) , StateBaseDir + string ( os . PathSeparator ) ) && repoDir != StateBaseDir {
105+ if ! pathutil . IsPathWithinBase (repoDir , StateBaseDir ) && repoDir != StateBaseDir {
105106 log .Printf ("contentGitAbandon: invalid repoPath resolved=%q base=%q" , repoDir , StateBaseDir )
106107 c .JSON (http .StatusBadRequest , gin.H {"error" : "invalid repoPath" })
107108 return
@@ -126,7 +127,7 @@ func ContentGitDiff(c *gin.Context) {
126127 }
127128
128129 repoDir := filepath .Clean (filepath .Join (StateBaseDir , repoPath ))
129- if ! strings . HasPrefix (repoDir + string ( os . PathSeparator ) , StateBaseDir + string ( os . PathSeparator ) ) && repoDir != StateBaseDir {
130+ if ! pathutil . IsPathWithinBase (repoDir , StateBaseDir ) && repoDir != StateBaseDir {
130131 c .JSON (http .StatusBadRequest , gin.H {"error" : "invalid repoPath" })
131132 return
132133 }
@@ -159,13 +160,13 @@ func ContentGitDiff(c *gin.Context) {
159160// ContentGitStatus handles GET /content/git-status?path=
160161func ContentGitStatus (c * gin.Context ) {
161162 path := filepath .Clean ("/" + strings .TrimSpace (c .Query ("path" )))
162- if path == "/" || strings .Contains (path , ".." ) {
163+ abs := filepath .Join (StateBaseDir , path )
164+ // Verify abs is within StateBaseDir to prevent path traversal
165+ if ! pathutil .IsPathWithinBase (abs , StateBaseDir ) {
163166 c .JSON (http .StatusBadRequest , gin.H {"error" : "invalid path" })
164167 return
165168 }
166169
167- abs := filepath .Join (StateBaseDir , path )
168-
169170 // Check if directory exists
170171 if info , err := os .Stat (abs ); err != nil || ! info .IsDir () {
171172 c .JSON (http .StatusOK , gin.H {
@@ -224,13 +225,13 @@ func ContentGitConfigureRemote(c *gin.Context) {
224225 }
225226
226227 path := filepath .Clean ("/" + body .Path )
227- if path == "/" || strings .Contains (path , ".." ) {
228+ abs := filepath .Join (StateBaseDir , path )
229+ // Verify abs is within StateBaseDir to prevent path traversal
230+ if ! pathutil .IsPathWithinBase (abs , StateBaseDir ) {
228231 c .JSON (http .StatusBadRequest , gin.H {"error" : "invalid path" })
229232 return
230233 }
231234
232- abs := filepath .Join (StateBaseDir , path )
233-
234235 // Check if directory exists
235236 if info , err := os .Stat (abs ); err != nil || ! info .IsDir () {
236237 c .JSON (http .StatusBadRequest , gin.H {"error" : "directory not found" })
@@ -301,13 +302,13 @@ func ContentGitSync(c *gin.Context) {
301302 }
302303
303304 path := filepath .Clean ("/" + body .Path )
304- if path == "/" || strings .Contains (path , ".." ) {
305+ abs := filepath .Join (StateBaseDir , path )
306+ // Verify abs is within StateBaseDir to prevent path traversal
307+ if ! pathutil .IsPathWithinBase (abs , StateBaseDir ) {
305308 c .JSON (http .StatusBadRequest , gin.H {"error" : "invalid path" })
306309 return
307310 }
308311
309- abs := filepath .Join (StateBaseDir , path )
310-
311312 // Check if git repo exists
312313 gitDir := filepath .Join (abs , ".git" )
313314 if _ , err := os .Stat (gitDir ); err != nil {
@@ -345,12 +346,13 @@ func ContentWrite(c *gin.Context) {
345346 log .Printf ("ContentWrite: path=%q contentLen=%d encoding=%q StateBaseDir=%q" , req .Path , len (req .Content ), req .Encoding , StateBaseDir )
346347
347348 path := filepath .Clean ("/" + strings .TrimSpace (req .Path ))
348- if path == "/" || strings .Contains (path , ".." ) {
349- log .Printf ("ContentWrite: invalid path rejected: path=%q" , path )
349+ abs := filepath .Join (StateBaseDir , path )
350+ // Verify abs is within StateBaseDir to prevent path traversal
351+ if ! pathutil .IsPathWithinBase (abs , StateBaseDir ) {
352+ log .Printf ("ContentWrite: path traversal attempt rejected: path=%q abs=%q" , path , abs )
350353 c .JSON (http .StatusBadRequest , gin.H {"error" : "invalid path" })
351354 return
352355 }
353- abs := filepath .Join (StateBaseDir , path )
354356 log .Printf ("ContentWrite: absolute path=%q" , abs )
355357
356358 if err := os .MkdirAll (filepath .Dir (abs ), 0755 ); err != nil {
@@ -385,12 +387,13 @@ func ContentRead(c *gin.Context) {
385387 log .Printf ("ContentRead: requested path=%q StateBaseDir=%q" , c .Query ("path" ), StateBaseDir )
386388 log .Printf ("ContentRead: cleaned path=%q" , path )
387389
388- if path == "/" || strings .Contains (path , ".." ) {
389- log .Printf ("ContentRead: invalid path rejected: path=%q" , path )
390+ abs := filepath .Join (StateBaseDir , path )
391+ // Verify abs is within StateBaseDir to prevent path traversal
392+ if ! pathutil .IsPathWithinBase (abs , StateBaseDir ) {
393+ log .Printf ("ContentRead: path traversal attempt rejected: path=%q abs=%q" , path , abs )
390394 c .JSON (http .StatusBadRequest , gin.H {"error" : "invalid path" })
391395 return
392396 }
393- abs := filepath .Join (StateBaseDir , path )
394397 log .Printf ("ContentRead: absolute path=%q" , abs )
395398
396399 b , err := os .ReadFile (abs )
@@ -414,12 +417,13 @@ func ContentList(c *gin.Context) {
414417 log .Printf ("ContentList: cleaned path=%q" , path )
415418 log .Printf ("ContentList: StateBaseDir=%q" , StateBaseDir )
416419
417- if path == "/" || strings .Contains (path , ".." ) {
418- log .Printf ("ContentList: invalid path rejected: path=%q" , path )
420+ abs := filepath .Join (StateBaseDir , path )
421+ // Verify abs is within StateBaseDir to prevent path traversal
422+ if ! pathutil .IsPathWithinBase (abs , StateBaseDir ) {
423+ log .Printf ("ContentList: path traversal attempt rejected: path=%q abs=%q" , path , abs )
419424 c .JSON (http .StatusBadRequest , gin.H {"error" : "invalid path" })
420425 return
421426 }
422- abs := filepath .Join (StateBaseDir , path )
423427 log .Printf ("ContentList: absolute path=%q" , abs )
424428
425429 info , err := os .Stat (abs )
@@ -672,7 +676,9 @@ func ContentGitMergeStatus(c *gin.Context) {
672676 path := filepath .Clean ("/" + strings .TrimSpace (c .Query ("path" )))
673677 branch := strings .TrimSpace (c .Query ("branch" ))
674678
675- if path == "/" || strings .Contains (path , ".." ) {
679+ abs := filepath .Join (StateBaseDir , path )
680+ // Verify abs is within StateBaseDir to prevent path traversal
681+ if ! pathutil .IsPathWithinBase (abs , StateBaseDir ) {
676682 c .JSON (http .StatusBadRequest , gin.H {"error" : "invalid path" })
677683 return
678684 }
@@ -681,8 +687,6 @@ func ContentGitMergeStatus(c *gin.Context) {
681687 branch = "main"
682688 }
683689
684- abs := filepath .Join (StateBaseDir , path )
685-
686690 // Check if git repo exists
687691 gitDir := filepath .Join (abs , ".git" )
688692 if _ , err := os .Stat (gitDir ); err != nil {
@@ -722,7 +726,9 @@ func ContentGitPull(c *gin.Context) {
722726 }
723727
724728 path := filepath .Clean ("/" + body .Path )
725- if path == "/" || strings .Contains (path , ".." ) {
729+ abs := filepath .Join (StateBaseDir , path )
730+ // Verify abs is within StateBaseDir to prevent path traversal
731+ if ! pathutil .IsPathWithinBase (abs , StateBaseDir ) {
726732 c .JSON (http .StatusBadRequest , gin.H {"error" : "invalid path" })
727733 return
728734 }
@@ -731,8 +737,6 @@ func ContentGitPull(c *gin.Context) {
731737 body .Branch = "main"
732738 }
733739
734- abs := filepath .Join (StateBaseDir , path )
735-
736740 if err := GitPullRepo (c .Request .Context (), abs , body .Branch ); err != nil {
737741 c .JSON (http .StatusBadRequest , gin.H {"error" : err .Error ()})
738742 return
@@ -757,7 +761,9 @@ func ContentGitPushToBranch(c *gin.Context) {
757761 }
758762
759763 path := filepath .Clean ("/" + body .Path )
760- if path == "/" || strings .Contains (path , ".." ) {
764+ abs := filepath .Join (StateBaseDir , path )
765+ // Verify abs is within StateBaseDir to prevent path traversal
766+ if ! pathutil .IsPathWithinBase (abs , StateBaseDir ) {
761767 c .JSON (http .StatusBadRequest , gin.H {"error" : "invalid path" })
762768 return
763769 }
@@ -770,8 +776,6 @@ func ContentGitPushToBranch(c *gin.Context) {
770776 body .Message = "Session artifacts update"
771777 }
772778
773- abs := filepath .Join (StateBaseDir , path )
774-
775779 if err := GitPushToRepo (c .Request .Context (), abs , body .Branch , body .Message ); err != nil {
776780 c .JSON (http .StatusBadRequest , gin.H {"error" : err .Error ()})
777781 return
@@ -795,7 +799,9 @@ func ContentGitCreateBranch(c *gin.Context) {
795799 }
796800
797801 path := filepath .Clean ("/" + body .Path )
798- if path == "/" || strings .Contains (path , ".." ) {
802+ abs := filepath .Join (StateBaseDir , path )
803+ // Verify abs is within StateBaseDir to prevent path traversal
804+ if ! pathutil .IsPathWithinBase (abs , StateBaseDir ) {
799805 c .JSON (http .StatusBadRequest , gin.H {"error" : "invalid path" })
800806 return
801807 }
@@ -805,8 +811,6 @@ func ContentGitCreateBranch(c *gin.Context) {
805811 return
806812 }
807813
808- abs := filepath .Join (StateBaseDir , path )
809-
810814 if err := GitCreateBranch (c .Request .Context (), abs , body .BranchName ); err != nil {
811815 c .JSON (http .StatusBadRequest , gin.H {"error" : err .Error ()})
812816 return
@@ -820,13 +824,13 @@ func ContentGitCreateBranch(c *gin.Context) {
820824func ContentGitListBranches (c * gin.Context ) {
821825 path := filepath .Clean ("/" + strings .TrimSpace (c .Query ("path" )))
822826
823- if path == "/" || strings .Contains (path , ".." ) {
827+ abs := filepath .Join (StateBaseDir , path )
828+ // Verify abs is within StateBaseDir to prevent path traversal
829+ if ! pathutil .IsPathWithinBase (abs , StateBaseDir ) {
824830 c .JSON (http .StatusBadRequest , gin.H {"error" : "invalid path" })
825831 return
826832 }
827833
828- abs := filepath .Join (StateBaseDir , path )
829-
830834 branches , err := GitListRemoteBranches (c .Request .Context (), abs )
831835 if err != nil {
832836 // Log actual error for debugging, but return generic message to avoid leaking internal details
@@ -837,3 +841,43 @@ func ContentGitListBranches(c *gin.Context) {
837841
838842 c .JSON (http .StatusOK , gin.H {"branches" : branches })
839843}
844+
845+ // ContentDelete handles DELETE /content/delete when running in CONTENT_SERVICE_MODE
846+ func ContentDelete (c * gin.Context ) {
847+ var req struct {
848+ Path string `json:"path"`
849+ }
850+ if err := c .ShouldBindJSON (& req ); err != nil {
851+ log .Printf ("ContentDelete: bind JSON failed: %v" , err )
852+ c .JSON (http .StatusBadRequest , gin.H {"error" : err .Error ()})
853+ return
854+ }
855+ log .Printf ("ContentDelete: path=%q StateBaseDir=%q" , req .Path , StateBaseDir )
856+
857+ path := filepath .Clean ("/" + strings .TrimSpace (req .Path ))
858+ abs := filepath .Join (StateBaseDir , path )
859+ // Verify abs is within StateBaseDir to prevent path traversal
860+ if ! pathutil .IsPathWithinBase (abs , StateBaseDir ) {
861+ log .Printf ("ContentDelete: path traversal attempt rejected: path=%q abs=%q" , path , abs )
862+ c .JSON (http .StatusBadRequest , gin.H {"error" : "invalid path" })
863+ return
864+ }
865+ log .Printf ("ContentDelete: absolute path=%q" , abs )
866+
867+ // Check if file exists
868+ if _ , err := os .Stat (abs ); os .IsNotExist (err ) {
869+ log .Printf ("ContentDelete: file not found: %q" , abs )
870+ c .JSON (http .StatusNotFound , gin.H {"error" : "file not found" })
871+ return
872+ }
873+
874+ // Delete the file
875+ if err := os .Remove (abs ); err != nil {
876+ log .Printf ("ContentDelete: delete failed for %q: %v" , abs , err )
877+ c .JSON (http .StatusInternalServerError , gin.H {"error" : "failed to delete file" })
878+ return
879+ }
880+
881+ log .Printf ("ContentDelete: successfully deleted %q" , abs )
882+ c .JSON (http .StatusOK , gin.H {"message" : "file deleted successfully" })
883+ }
0 commit comments