Skip to content

Commit 00c4db4

Browse files
committed
set MAX_FILE_SIZE for images & update logic for path traversal
Signed-off-by: sallyom <somalley@redhat.com>
1 parent c577a71 commit 00c4db4

File tree

3 files changed

+93
-53
lines changed
  • components

3 files changed

+93
-53
lines changed

components/backend/handlers/content.go

Lines changed: 40 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -159,13 +159,13 @@ func ContentGitDiff(c *gin.Context) {
159159
// ContentGitStatus handles GET /content/git-status?path=
160160
func ContentGitStatus(c *gin.Context) {
161161
path := filepath.Clean("/" + strings.TrimSpace(c.Query("path")))
162-
if path == "/" || strings.Contains(path, "..") {
162+
abs := filepath.Join(StateBaseDir, path)
163+
// Verify abs is within StateBaseDir to prevent path traversal
164+
if !strings.HasPrefix(abs, filepath.Clean(StateBaseDir)+string(os.PathSeparator)) {
163165
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
164166
return
165167
}
166168

167-
abs := filepath.Join(StateBaseDir, path)
168-
169169
// Check if directory exists
170170
if info, err := os.Stat(abs); err != nil || !info.IsDir() {
171171
c.JSON(http.StatusOK, gin.H{
@@ -224,13 +224,13 @@ func ContentGitConfigureRemote(c *gin.Context) {
224224
}
225225

226226
path := filepath.Clean("/" + body.Path)
227-
if path == "/" || strings.Contains(path, "..") {
227+
abs := filepath.Join(StateBaseDir, path)
228+
// Verify abs is within StateBaseDir to prevent path traversal
229+
if !strings.HasPrefix(abs, filepath.Clean(StateBaseDir)+string(os.PathSeparator)) {
228230
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
229231
return
230232
}
231233

232-
abs := filepath.Join(StateBaseDir, path)
233-
234234
// Check if directory exists
235235
if info, err := os.Stat(abs); err != nil || !info.IsDir() {
236236
c.JSON(http.StatusBadRequest, gin.H{"error": "directory not found"})
@@ -301,13 +301,13 @@ func ContentGitSync(c *gin.Context) {
301301
}
302302

303303
path := filepath.Clean("/" + body.Path)
304-
if path == "/" || strings.Contains(path, "..") {
304+
abs := filepath.Join(StateBaseDir, path)
305+
// Verify abs is within StateBaseDir to prevent path traversal
306+
if !strings.HasPrefix(abs, filepath.Clean(StateBaseDir)+string(os.PathSeparator)) {
305307
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
306308
return
307309
}
308310

309-
abs := filepath.Join(StateBaseDir, path)
310-
311311
// Check if git repo exists
312312
gitDir := filepath.Join(abs, ".git")
313313
if _, err := os.Stat(gitDir); err != nil {
@@ -343,12 +343,13 @@ func ContentWrite(c *gin.Context) {
343343
log.Printf("ContentWrite: path=%q contentLen=%d encoding=%q StateBaseDir=%q", req.Path, len(req.Content), req.Encoding, StateBaseDir)
344344

345345
path := filepath.Clean("/" + strings.TrimSpace(req.Path))
346-
if path == "/" || strings.Contains(path, "..") {
347-
log.Printf("ContentWrite: invalid path rejected: path=%q", path)
346+
abs := filepath.Join(StateBaseDir, path)
347+
// Verify abs is within StateBaseDir to prevent path traversal
348+
if !strings.HasPrefix(abs, filepath.Clean(StateBaseDir)+string(os.PathSeparator)) {
349+
log.Printf("ContentWrite: path traversal attempt rejected: path=%q abs=%q", path, abs)
348350
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
349351
return
350352
}
351-
abs := filepath.Join(StateBaseDir, path)
352353
log.Printf("ContentWrite: absolute path=%q", abs)
353354

354355
if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil {
@@ -383,12 +384,13 @@ func ContentRead(c *gin.Context) {
383384
log.Printf("ContentRead: requested path=%q StateBaseDir=%q", c.Query("path"), StateBaseDir)
384385
log.Printf("ContentRead: cleaned path=%q", path)
385386

386-
if path == "/" || strings.Contains(path, "..") {
387-
log.Printf("ContentRead: invalid path rejected: path=%q", path)
387+
abs := filepath.Join(StateBaseDir, path)
388+
// Verify abs is within StateBaseDir to prevent path traversal
389+
if !strings.HasPrefix(abs, filepath.Clean(StateBaseDir)+string(os.PathSeparator)) {
390+
log.Printf("ContentRead: path traversal attempt rejected: path=%q abs=%q", path, abs)
388391
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
389392
return
390393
}
391-
abs := filepath.Join(StateBaseDir, path)
392394
log.Printf("ContentRead: absolute path=%q", abs)
393395

394396
b, err := os.ReadFile(abs)
@@ -412,12 +414,13 @@ func ContentList(c *gin.Context) {
412414
log.Printf("ContentList: cleaned path=%q", path)
413415
log.Printf("ContentList: StateBaseDir=%q", StateBaseDir)
414416

415-
if path == "/" || strings.Contains(path, "..") {
416-
log.Printf("ContentList: invalid path rejected: path=%q", path)
417+
abs := filepath.Join(StateBaseDir, path)
418+
// Verify abs is within StateBaseDir to prevent path traversal
419+
if !strings.HasPrefix(abs, filepath.Clean(StateBaseDir)+string(os.PathSeparator)) {
420+
log.Printf("ContentList: path traversal attempt rejected: path=%q abs=%q", path, abs)
417421
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
418422
return
419423
}
420-
abs := filepath.Join(StateBaseDir, path)
421424
log.Printf("ContentList: absolute path=%q", abs)
422425

423426
info, err := os.Stat(abs)
@@ -670,7 +673,9 @@ func ContentGitMergeStatus(c *gin.Context) {
670673
path := filepath.Clean("/" + strings.TrimSpace(c.Query("path")))
671674
branch := strings.TrimSpace(c.Query("branch"))
672675

673-
if path == "/" || strings.Contains(path, "..") {
676+
abs := filepath.Join(StateBaseDir, path)
677+
// Verify abs is within StateBaseDir to prevent path traversal
678+
if !strings.HasPrefix(abs, filepath.Clean(StateBaseDir)+string(os.PathSeparator)) {
674679
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
675680
return
676681
}
@@ -679,8 +684,6 @@ func ContentGitMergeStatus(c *gin.Context) {
679684
branch = "main"
680685
}
681686

682-
abs := filepath.Join(StateBaseDir, path)
683-
684687
// Check if git repo exists
685688
gitDir := filepath.Join(abs, ".git")
686689
if _, err := os.Stat(gitDir); err != nil {
@@ -718,7 +721,9 @@ func ContentGitPull(c *gin.Context) {
718721
}
719722

720723
path := filepath.Clean("/" + body.Path)
721-
if path == "/" || strings.Contains(path, "..") {
724+
abs := filepath.Join(StateBaseDir, path)
725+
// Verify abs is within StateBaseDir to prevent path traversal
726+
if !strings.HasPrefix(abs, filepath.Clean(StateBaseDir)+string(os.PathSeparator)) {
722727
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
723728
return
724729
}
@@ -727,8 +732,6 @@ func ContentGitPull(c *gin.Context) {
727732
body.Branch = "main"
728733
}
729734

730-
abs := filepath.Join(StateBaseDir, path)
731-
732735
if err := GitPullRepo(c.Request.Context(), abs, body.Branch); err != nil {
733736
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
734737
return
@@ -753,7 +756,9 @@ func ContentGitPushToBranch(c *gin.Context) {
753756
}
754757

755758
path := filepath.Clean("/" + body.Path)
756-
if path == "/" || strings.Contains(path, "..") {
759+
abs := filepath.Join(StateBaseDir, path)
760+
// Verify abs is within StateBaseDir to prevent path traversal
761+
if !strings.HasPrefix(abs, filepath.Clean(StateBaseDir)+string(os.PathSeparator)) {
757762
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
758763
return
759764
}
@@ -766,8 +771,6 @@ func ContentGitPushToBranch(c *gin.Context) {
766771
body.Message = "Session artifacts update"
767772
}
768773

769-
abs := filepath.Join(StateBaseDir, path)
770-
771774
if err := GitPushToRepo(c.Request.Context(), abs, body.Branch, body.Message); err != nil {
772775
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
773776
return
@@ -791,7 +794,9 @@ func ContentGitCreateBranch(c *gin.Context) {
791794
}
792795

793796
path := filepath.Clean("/" + body.Path)
794-
if path == "/" || strings.Contains(path, "..") {
797+
abs := filepath.Join(StateBaseDir, path)
798+
// Verify abs is within StateBaseDir to prevent path traversal
799+
if !strings.HasPrefix(abs, filepath.Clean(StateBaseDir)+string(os.PathSeparator)) {
795800
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
796801
return
797802
}
@@ -801,8 +806,6 @@ func ContentGitCreateBranch(c *gin.Context) {
801806
return
802807
}
803808

804-
abs := filepath.Join(StateBaseDir, path)
805-
806809
if err := GitCreateBranch(c.Request.Context(), abs, body.BranchName); err != nil {
807810
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
808811
return
@@ -816,13 +819,13 @@ func ContentGitCreateBranch(c *gin.Context) {
816819
func ContentGitListBranches(c *gin.Context) {
817820
path := filepath.Clean("/" + strings.TrimSpace(c.Query("path")))
818821

819-
if path == "/" || strings.Contains(path, "..") {
822+
abs := filepath.Join(StateBaseDir, path)
823+
// Verify abs is within StateBaseDir to prevent path traversal
824+
if !strings.HasPrefix(abs, filepath.Clean(StateBaseDir)+string(os.PathSeparator)) {
820825
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
821826
return
822827
}
823828

824-
abs := filepath.Join(StateBaseDir, path)
825-
826829
branches, err := GitListRemoteBranches(c.Request.Context(), abs)
827830
if err != nil {
828831
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
@@ -845,12 +848,13 @@ func ContentDelete(c *gin.Context) {
845848
log.Printf("ContentDelete: path=%q StateBaseDir=%q", req.Path, StateBaseDir)
846849

847850
path := filepath.Clean("/" + strings.TrimSpace(req.Path))
848-
if path == "/" || strings.Contains(path, "..") {
849-
log.Printf("ContentDelete: invalid path rejected: path=%q", path)
851+
abs := filepath.Join(StateBaseDir, path)
852+
// Verify abs is within StateBaseDir to prevent path traversal
853+
if !strings.HasPrefix(abs, filepath.Clean(StateBaseDir)+string(os.PathSeparator)) {
854+
log.Printf("ContentDelete: path traversal attempt rejected: path=%q abs=%q", path, abs)
850855
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
851856
return
852857
}
853-
abs := filepath.Join(StateBaseDir, path)
854858
log.Printf("ContentDelete: absolute path=%q", abs)
855859

856860
// Check if file exists

components/frontend/src/app/api/projects/[name]/agentic-sessions/[sessionName]/workspace/upload/route.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,26 @@ import { buildForwardHeadersAsync } from '@/lib/auth';
22
import { BACKEND_URL } from '@/lib/config';
33
import { NextRequest } from 'next/server';
44

5-
// Maximum file size: 10MB
6-
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB in bytes
5+
// Maximum file sizes based on type
6+
const MAX_DOCUMENT_SIZE = 8 * 1024 * 1024; // 8MB for documents
7+
const MAX_IMAGE_SIZE = 1 * 1024 * 1024; // 1MB for images
8+
9+
// Determine if a file is an image based on content type
10+
const isImageFile = (contentType: string): boolean => {
11+
return contentType.startsWith('image/');
12+
};
13+
14+
// Get the appropriate max file size based on content type
15+
const getMaxFileSize = (contentType: string): number => {
16+
return isImageFile(contentType) ? MAX_IMAGE_SIZE : MAX_DOCUMENT_SIZE;
17+
};
18+
19+
// Format size limit for error messages
20+
const formatSizeLimit = (contentType: string): string => {
21+
const sizeInMB = getMaxFileSize(contentType) / (1024 * 1024);
22+
const fileType = isImageFile(contentType) ? 'images' : 'documents';
23+
return `${sizeInMB}MB for ${fileType}`;
24+
};
725

826
export async function POST(
927
request: NextRequest,
@@ -27,12 +45,14 @@ export async function POST(
2745
}
2846

2947
const filename = (formData.get('filename') as string) || file.name;
48+
const contentType = file.type || 'application/octet-stream';
3049

31-
// Check file size
32-
if (file.size > MAX_FILE_SIZE) {
50+
// Check file size based on type
51+
const maxSize = getMaxFileSize(contentType);
52+
if (file.size > maxSize) {
3353
return new Response(
3454
JSON.stringify({
35-
error: `File too large. Maximum size is ${MAX_FILE_SIZE / (1024 * 1024)}MB`
55+
error: `File too large. Maximum size is ${formatSizeLimit(contentType)}`
3656
}),
3757
{
3858
status: 413, // Payload Too Large
@@ -114,12 +134,14 @@ export async function POST(
114134
}
115135

116136
const fileBuffer = await fileResp.arrayBuffer();
137+
const contentType = fileResp.headers.get('content-type') || 'application/octet-stream';
117138

118-
// Check file size
119-
if (fileBuffer.byteLength > MAX_FILE_SIZE) {
139+
// Check file size based on type
140+
const maxSize = getMaxFileSize(contentType);
141+
if (fileBuffer.byteLength > maxSize) {
120142
return new Response(
121143
JSON.stringify({
122-
error: `File too large. Maximum size is ${MAX_FILE_SIZE / (1024 * 1024)}MB`
144+
error: `File too large. Maximum size is ${formatSizeLimit(contentType)}`
123145
}),
124146
{
125147
status: 413, // Payload Too Large
@@ -128,8 +150,6 @@ export async function POST(
128150
);
129151
}
130152

131-
const contentType = fileResp.headers.get('content-type') || 'application/octet-stream';
132-
133153
// Upload to workspace/file-uploads directory using the PUT endpoint
134154
// Retry logic: if backend returns 202 (content service starting), retry up to 3 times
135155
let resp: Response | null = null;

components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/modals/upload-file-modal.tsx

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,25 @@ import { Label } from "@/components/ui/label";
1616
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
1717
import { Alert, AlertDescription } from "@/components/ui/alert";
1818

19-
// Maximum file size: 10MB (matches backend limit)
20-
const MAX_FILE_SIZE = 10 * 1024 * 1024;
19+
// Maximum file sizes based on type
20+
const MAX_DOCUMENT_SIZE = 8 * 1024 * 1024; // 8MB for documents
21+
const MAX_IMAGE_SIZE = 1 * 1024 * 1024; // 1MB for images
22+
23+
// Determine if a file is an image based on MIME type
24+
const isImageFile = (fileType: string): boolean => {
25+
return fileType.startsWith('image/');
26+
};
27+
28+
// Get the appropriate max file size based on file type
29+
const getMaxFileSize = (fileType: string): number => {
30+
return isImageFile(fileType) ? MAX_IMAGE_SIZE : MAX_DOCUMENT_SIZE;
31+
};
2132

2233
const formatFileSize = (bytes: number): string => {
2334
if (bytes < 1024) return `${bytes} B`;
2435
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
25-
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
36+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
37+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
2638
};
2739

2840
type UploadFileModalProps = {
@@ -107,10 +119,14 @@ export function UploadFileModal({
107119
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
108120
const file = e.target.files?.[0];
109121
if (file) {
110-
// Check file size
111-
if (file.size > MAX_FILE_SIZE) {
122+
const fileType = file.type || 'application/octet-stream';
123+
const maxSize = getMaxFileSize(fileType);
124+
const fileTypeLabel = isImageFile(fileType) ? 'images' : 'documents';
125+
126+
// Check file size based on type
127+
if (file.size > maxSize) {
112128
setFileSizeError(
113-
`File size (${formatFileSize(file.size)}) exceeds maximum allowed size of ${formatFileSize(MAX_FILE_SIZE)}`
129+
`File size (${formatFileSize(file.size)}) exceeds maximum allowed size of ${formatFileSize(maxSize)} for ${fileTypeLabel}`
114130
);
115131
setSelectedFile(null);
116132
if (fileInputRef.current) {
@@ -137,7 +153,7 @@ export function UploadFileModal({
137153
<DialogTitle>Upload File</DialogTitle>
138154
<DialogDescription>
139155
Upload files to your workspace from your local machine or a URL. Files will be available in
140-
the file-uploads folder. Maximum file size: {formatFileSize(MAX_FILE_SIZE)}.
156+
the file-uploads folder. Maximum file size: {formatFileSize(MAX_IMAGE_SIZE)} for images, {formatFileSize(MAX_DOCUMENT_SIZE)} for documents.
141157
</DialogDescription>
142158
</DialogHeader>
143159

0 commit comments

Comments
 (0)