Skip to content

Commit cb0de36

Browse files
author
Guillermo Villar
committed
fixed security issue with how images were served
1 parent 15ff45d commit cb0de36

File tree

4 files changed

+108
-26
lines changed

4 files changed

+108
-26
lines changed

backend/main.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,6 @@
8080
static_path = Path(__file__).parent.parent / "frontend"
8181
app.mount("/static", StaticFiles(directory=static_path), name="static")
8282

83-
# Mount data directory for serving images
84-
data_path = Path(config['storage']['notes_dir'])
85-
app.mount("/data", StaticFiles(directory=data_path), name="data")
86-
8783

8884
# ============================================================================
8985
# Custom Exception Handlers
@@ -438,6 +434,36 @@ async def create_new_folder(data: dict):
438434
raise HTTPException(status_code=500, detail=str(e))
439435

440436

437+
@api_router.get("/images/{image_path:path}")
438+
async def get_image(image_path: str):
439+
"""
440+
Serve an image file with authentication protection.
441+
"""
442+
try:
443+
notes_dir = config['storage']['notes_dir']
444+
full_path = Path(notes_dir) / image_path
445+
446+
# Security: Validate path is within notes directory
447+
if not validate_path_security(notes_dir, full_path):
448+
raise HTTPException(status_code=403, detail="Access denied")
449+
450+
# Check file exists and is an image
451+
if not full_path.exists() or not full_path.is_file():
452+
raise HTTPException(status_code=404, detail="Image not found")
453+
454+
# Validate it's an image file
455+
allowed_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.webp'}
456+
if full_path.suffix.lower() not in allowed_extensions:
457+
raise HTTPException(status_code=400, detail="Not an image file")
458+
459+
# Return the file
460+
return FileResponse(full_path)
461+
except HTTPException:
462+
raise
463+
except Exception as e:
464+
raise HTTPException(status_code=500, detail=str(e))
465+
466+
441467
@api_router.post("/upload-image")
442468
async def upload_image(file: UploadFile = File(...), note_path: str = Form(...)):
443469
"""

documentation/API.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,67 @@ Content-Type: application/json
8181
}
8282
```
8383

84+
## 🖼️ Images
85+
86+
### Get Image
87+
```http
88+
GET /api/images/{image_path}
89+
```
90+
Retrieve an image file with authentication protection.
91+
92+
**Example:**
93+
```bash
94+
curl http://localhost:8000/api/images/folder/_attachments/image-20240417093343.png
95+
```
96+
97+
**Security Note:** This endpoint requires authentication and validates that:
98+
- The image path is within the notes directory (prevents directory traversal)
99+
- The file exists and is a valid image format
100+
- The requesting user is authenticated (if auth is enabled)
101+
102+
### Upload Image
103+
```http
104+
POST /api/upload-image
105+
Content-Type: multipart/form-data
106+
107+
file: <image file>
108+
note_path: <path of note to attach to>
109+
```
110+
111+
Upload an image file to the `_attachments` directory. Images are automatically organized per-folder and named with timestamps to prevent conflicts.
112+
113+
**Supported formats:** JPG, JPEG, PNG, GIF, WEBP
114+
**Maximum size:** 10MB
115+
116+
**Response:**
117+
```json
118+
{
119+
"success": true,
120+
"path": "folder/_attachments/image-20240417093343.png",
121+
"filename": "image-20240417093343.png",
122+
"message": "Image uploaded successfully"
123+
}
124+
```
125+
126+
**Example (using curl):**
127+
```bash
128+
curl -X POST http://localhost:8000/api/upload-image \
129+
-F "file=@/path/to/image.png" \
130+
-F "note_path=folder/mynote.md"
131+
```
132+
133+
**Windows PowerShell:**
134+
```powershell
135+
curl.exe -X POST http://localhost:8000/api/upload-image -F "file=@C:\path\to\image.png" -F "note_path=folder/mynote.md"
136+
```
137+
138+
**Notes:**
139+
- Images are stored in `_attachments` folders relative to the note's location
140+
- Filenames are automatically timestamped (e.g., `image-20240417093343.png`)
141+
- Images appear in the sidebar navigation and can be viewed/deleted directly
142+
- Drag & drop images into the editor automatically uploads and inserts markdown
143+
- All image access requires authentication when security is enabled
144+
84145
## 📁 Folders
85146

86147
### Create Folder
@@ -93,6 +154,17 @@ Content-Type: application/json
93154
}
94155
```
95156

157+
### Delete Folder
158+
```http
159+
DELETE /api/folders/{folder_path}
160+
```
161+
Deletes a folder and all its contents.
162+
163+
**Example:**
164+
```bash
165+
curl -X DELETE http://localhost:8000/api/folders/Projects/Archive
166+
```
167+
96168
### Move Folder
97169
```http
98170
POST /api/folders/move

frontend/app.js

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -787,7 +787,7 @@ function noteApp() {
787787
const filename = notePath.split('/').pop().replace(/\.[^/.]+$/, ''); // Remove extension
788788
// URL-encode the path to handle spaces and special characters
789789
const encodedPath = notePath.split('/').map(segment => encodeURIComponent(segment)).join('/');
790-
link = `![${filename}](/data/${encodedPath})`;
790+
link = `![${filename}](/api/images/${encodedPath})`;
791791
} else {
792792
// For notes, insert note link
793793
const noteName = notePath.split('/').pop().replace('.md', '');
@@ -879,7 +879,7 @@ function noteApp() {
879879
const filename = altText.replace(/\.[^/.]+$/, ''); // Remove extension
880880
// URL-encode the path to handle spaces and special characters
881881
const encodedPath = imagePath.split('/').map(segment => encodeURIComponent(segment)).join('/');
882-
const markdown = `![${filename}](/data/${encodedPath})`;
882+
const markdown = `![${filename}](/api/images/${encodedPath})`;
883883

884884
const textBefore = this.noteContent.substring(0, cursorPos);
885885
const textAfter = this.noteContent.substring(cursorPos);
@@ -1242,7 +1242,7 @@ function noteApp() {
12421242
const path = window.location.pathname;
12431243

12441244
// Skip if root path or static assets
1245-
if (path === '/' || path.startsWith('/static/') || path.startsWith('/api/') || path.startsWith('/data/')) {
1245+
if (path === '/' || path.startsWith('/static/') || path.startsWith('/api/')) {
12461246
return;
12471247
}
12481248

@@ -1855,24 +1855,8 @@ function noteApp() {
18551855
async deleteCurrentNote() {
18561856
if (!this.currentNote) return;
18571857

1858-
if (!confirm(`Delete "${this.currentNoteName}"?`)) return;
1859-
1860-
try {
1861-
const response = await fetch(`/api/notes/${this.currentNote}`, {
1862-
method: 'DELETE'
1863-
});
1864-
1865-
if (response.ok) {
1866-
this.currentNote = '';
1867-
this.noteContent = '';
1868-
this.currentNoteName = '';
1869-
await this.loadNotes();
1870-
} else {
1871-
ErrorHandler.handle('delete note', new Error('Server returned error'));
1872-
}
1873-
} catch (error) {
1874-
ErrorHandler.handle('delete note', error);
1875-
}
1858+
// Just call deleteNote with current note details
1859+
await this.deleteNote(this.currentNote, this.currentNoteName);
18761860
},
18771861

18781862
// Delete any note from sidebar

frontend/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1197,7 +1197,7 @@ <h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary);" x-text="
11971197
style="background-color: var(--bg-primary); min-height: 0;"
11981198
>
11991199
<img
1200-
:src="`/data/${currentImage}`"
1200+
:src="`/api/images/${currentImage}`"
12011201
:alt="currentImage.split('/').pop()"
12021202
style="max-width: 100%; max-height: 100%; object-fit: contain; padding: 2rem;"
12031203
/>

0 commit comments

Comments
 (0)