fixed security issue with how images were served
This commit is contained in:
parent
4ff1f7e8bd
commit
62c93064de
|
|
@ -80,10 +80,6 @@ plugin_manager.run_hook('on_app_startup')
|
|||
static_path = Path(__file__).parent.parent / "frontend"
|
||||
app.mount("/static", StaticFiles(directory=static_path), name="static")
|
||||
|
||||
# Mount data directory for serving images
|
||||
data_path = Path(config['storage']['notes_dir'])
|
||||
app.mount("/data", StaticFiles(directory=data_path), name="data")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Custom Exception Handlers
|
||||
|
|
@ -438,6 +434,36 @@ async def create_new_folder(data: dict):
|
|||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@api_router.get("/images/{image_path:path}")
|
||||
async def get_image(image_path: str):
|
||||
"""
|
||||
Serve an image file with authentication protection.
|
||||
"""
|
||||
try:
|
||||
notes_dir = config['storage']['notes_dir']
|
||||
full_path = Path(notes_dir) / image_path
|
||||
|
||||
# Security: Validate path is within notes directory
|
||||
if not validate_path_security(notes_dir, full_path):
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
# Check file exists and is an image
|
||||
if not full_path.exists() or not full_path.is_file():
|
||||
raise HTTPException(status_code=404, detail="Image not found")
|
||||
|
||||
# Validate it's an image file
|
||||
allowed_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.webp'}
|
||||
if full_path.suffix.lower() not in allowed_extensions:
|
||||
raise HTTPException(status_code=400, detail="Not an image file")
|
||||
|
||||
# Return the file
|
||||
return FileResponse(full_path)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@api_router.post("/upload-image")
|
||||
async def upload_image(file: UploadFile = File(...), note_path: str = Form(...)):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -81,6 +81,67 @@ Content-Type: application/json
|
|||
}
|
||||
```
|
||||
|
||||
## 🖼️ Images
|
||||
|
||||
### Get Image
|
||||
```http
|
||||
GET /api/images/{image_path}
|
||||
```
|
||||
Retrieve an image file with authentication protection.
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
curl http://localhost:8000/api/images/folder/_attachments/image-20240417093343.png
|
||||
```
|
||||
|
||||
**Security Note:** This endpoint requires authentication and validates that:
|
||||
- The image path is within the notes directory (prevents directory traversal)
|
||||
- The file exists and is a valid image format
|
||||
- The requesting user is authenticated (if auth is enabled)
|
||||
|
||||
### Upload Image
|
||||
```http
|
||||
POST /api/upload-image
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
file: <image file>
|
||||
note_path: <path of note to attach to>
|
||||
```
|
||||
|
||||
Upload an image file to the `_attachments` directory. Images are automatically organized per-folder and named with timestamps to prevent conflicts.
|
||||
|
||||
**Supported formats:** JPG, JPEG, PNG, GIF, WEBP
|
||||
**Maximum size:** 10MB
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"path": "folder/_attachments/image-20240417093343.png",
|
||||
"filename": "image-20240417093343.png",
|
||||
"message": "Image uploaded successfully"
|
||||
}
|
||||
```
|
||||
|
||||
**Example (using curl):**
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/upload-image \
|
||||
-F "file=@/path/to/image.png" \
|
||||
-F "note_path=folder/mynote.md"
|
||||
```
|
||||
|
||||
**Windows PowerShell:**
|
||||
```powershell
|
||||
curl.exe -X POST http://localhost:8000/api/upload-image -F "file=@C:\path\to\image.png" -F "note_path=folder/mynote.md"
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Images are stored in `_attachments` folders relative to the note's location
|
||||
- Filenames are automatically timestamped (e.g., `image-20240417093343.png`)
|
||||
- Images appear in the sidebar navigation and can be viewed/deleted directly
|
||||
- Drag & drop images into the editor automatically uploads and inserts markdown
|
||||
- All image access requires authentication when security is enabled
|
||||
|
||||
## 📁 Folders
|
||||
|
||||
### Create Folder
|
||||
|
|
@ -93,6 +154,17 @@ Content-Type: application/json
|
|||
}
|
||||
```
|
||||
|
||||
### Delete Folder
|
||||
```http
|
||||
DELETE /api/folders/{folder_path}
|
||||
```
|
||||
Deletes a folder and all its contents.
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
curl -X DELETE http://localhost:8000/api/folders/Projects/Archive
|
||||
```
|
||||
|
||||
### Move Folder
|
||||
```http
|
||||
POST /api/folders/move
|
||||
|
|
|
|||
|
|
@ -787,7 +787,7 @@ function noteApp() {
|
|||
const filename = notePath.split('/').pop().replace(/\.[^/.]+$/, ''); // Remove extension
|
||||
// URL-encode the path to handle spaces and special characters
|
||||
const encodedPath = notePath.split('/').map(segment => encodeURIComponent(segment)).join('/');
|
||||
link = ``;
|
||||
link = ``;
|
||||
} else {
|
||||
// For notes, insert note link
|
||||
const noteName = notePath.split('/').pop().replace('.md', '');
|
||||
|
|
@ -879,7 +879,7 @@ function noteApp() {
|
|||
const filename = altText.replace(/\.[^/.]+$/, ''); // Remove extension
|
||||
// URL-encode the path to handle spaces and special characters
|
||||
const encodedPath = imagePath.split('/').map(segment => encodeURIComponent(segment)).join('/');
|
||||
const markdown = ``;
|
||||
const markdown = ``;
|
||||
|
||||
const textBefore = this.noteContent.substring(0, cursorPos);
|
||||
const textAfter = this.noteContent.substring(cursorPos);
|
||||
|
|
@ -1242,7 +1242,7 @@ function noteApp() {
|
|||
const path = window.location.pathname;
|
||||
|
||||
// Skip if root path or static assets
|
||||
if (path === '/' || path.startsWith('/static/') || path.startsWith('/api/') || path.startsWith('/data/')) {
|
||||
if (path === '/' || path.startsWith('/static/') || path.startsWith('/api/')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1855,24 +1855,8 @@ function noteApp() {
|
|||
async deleteCurrentNote() {
|
||||
if (!this.currentNote) return;
|
||||
|
||||
if (!confirm(`Delete "${this.currentNoteName}"?`)) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/notes/${this.currentNote}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.currentNote = '';
|
||||
this.noteContent = '';
|
||||
this.currentNoteName = '';
|
||||
await this.loadNotes();
|
||||
} else {
|
||||
ErrorHandler.handle('delete note', new Error('Server returned error'));
|
||||
}
|
||||
} catch (error) {
|
||||
ErrorHandler.handle('delete note', error);
|
||||
}
|
||||
// Just call deleteNote with current note details
|
||||
await this.deleteNote(this.currentNote, this.currentNoteName);
|
||||
},
|
||||
|
||||
// Delete any note from sidebar
|
||||
|
|
|
|||
|
|
@ -1197,7 +1197,7 @@
|
|||
style="background-color: var(--bg-primary); min-height: 0;"
|
||||
>
|
||||
<img
|
||||
:src="`/data/${currentImage}`"
|
||||
:src="`/api/images/${currentImage}`"
|
||||
:alt="currentImage.split('/').pop()"
|
||||
style="max-width: 100%; max-height: 100%; object-fit: contain; padding: 2rem;"
|
||||
/>
|
||||
|
|
|
|||
Loading…
Reference in New Issue