diff --git a/README.md b/README.md index ece1a1e..a6332ea 100644 --- a/README.md +++ b/README.md @@ -68,11 +68,6 @@ Invoke-RestMethod -Uri "https://linedump.com/{path}" Invoke-RestMethod -Uri "https://linedump.com/{path}" -OutFile "filename.txt" # save to file - █ Delete: - -curl -X POST "https://linedump.com/{path}?token={deletion_token}" # delete paste - - ██ Encryption Examples with curl ██ @@ -146,7 +141,7 @@ Invoke-RestMethod -Uri "https://linedump.com/" -Headers @{"Authorization"="Beare **Simple / Testing** -`docker run -d -p 127.0.0.1:8000:8000 -v /path/to/uploads:/app/uploads git.uphillsecurity.com/cf7/linedump:latest` +`docker run -d -p 127.0.0.1:8000:8000 -v /path/to/upload:/app/upload git.uphillsecurity.com/cf7/linedump:latest` Open `http://127.0.0.1:8000` diff --git a/main.py b/main.py index dad094b..f535adf 100644 --- a/main.py +++ b/main.py @@ -107,92 +107,6 @@ def generate_random_path(length: int = None) -> str: return ''.join(secrets.choice(alphabet) for _ in range(length)) -def generate_deletion_token() -> str: - """Generate a secure deletion token""" - return secrets.token_urlsafe(32) - - -def validate_paste_id(paste_id: str) -> bool: - """Validate paste ID to prevent path traversal and other attacks""" - # Must be alphanumeric only - if not paste_id.isalnum(): - return False - # Reasonable length check (prevent extremely long IDs) - if len(paste_id) > 64: - return False - # Must not be empty - if len(paste_id) == 0: - return False - return True - - -def save_metadata(paste_id: str, deletion_token: str, client_ip: str) -> None: - """Save paste metadata to JSON file""" - # Validate paste_id before file operations - if not validate_paste_id(paste_id): - raise ValueError("Invalid paste ID") - - meta_path = UPLOAD_DIR / f"{paste_id}.meta" - - # Ensure resolved path is within UPLOAD_DIR (prevent path traversal) - if not str(meta_path.resolve()).startswith(str(UPLOAD_DIR.resolve())): - raise ValueError("Invalid paste ID: path traversal detected") - - metadata = { - "deletion_token": deletion_token, - "created_at": datetime.utcnow().isoformat() + "Z", - "client_ip": client_ip - } - with open(meta_path, 'w') as f: - json.dump(metadata, f) - - -def load_metadata(paste_id: str) -> Optional[dict]: - """Load paste metadata from JSON file""" - # Validate paste_id before file operations - if not validate_paste_id(paste_id): - return None - - meta_path = UPLOAD_DIR / f"{paste_id}.meta" - - # Ensure resolved path is within UPLOAD_DIR (prevent path traversal) - if not str(meta_path.resolve()).startswith(str(UPLOAD_DIR.resolve())): - return None - - if not meta_path.exists(): - return None - try: - with open(meta_path, 'r') as f: - return json.load(f) - except: - return None - - -def delete_paste(paste_id: str) -> bool: - """Delete paste and its metadata""" - # Validate paste_id before file operations - if not validate_paste_id(paste_id): - return False - - paste_path = UPLOAD_DIR / paste_id - meta_path = UPLOAD_DIR / f"{paste_id}.meta" - - # Ensure resolved paths are within UPLOAD_DIR (prevent path traversal) - if not str(paste_path.resolve()).startswith(str(UPLOAD_DIR.resolve())): - return False - if not str(meta_path.resolve()).startswith(str(UPLOAD_DIR.resolve())): - return False - - deleted = False - if paste_path.exists(): - paste_path.unlink() - deleted = True - if meta_path.exists(): - meta_path.unlink() - - return deleted - - def validate_upload_token(request: Request) -> bool: """Validate upload token if authentication is enabled""" if not UPLOAD_TOKENS: @@ -275,24 +189,16 @@ async def upload_text(request: Request, authorized: bool = Depends(validate_uplo file_path = UPLOAD_DIR / random_path try: - # Generate deletion token - deletion_token = generate_deletion_token() - - # Save paste content with open(file_path, 'w', encoding='utf-8') as f: f.write(content) - # Save metadata with deletion token - save_metadata(random_path, deletion_token, client_ip) - log("INFO", "paste_created", paste_id=random_path, client_ip=client_ip, user_agent=user_agent, size_bytes=len(content)) - # Return URL and deletion token - return f"{BASEURL}/{random_path}\nDelete with HTTP POST: {BASEURL}/{random_path}?token={deletion_token}\n" + return f"{BASEURL}/{random_path}\n" except Exception as e: log("ERROR", "upload_failed", @@ -302,16 +208,15 @@ async def upload_text(request: Request, authorized: bool = Depends(validate_uplo error=str(e)) raise HTTPException(status_code=500, detail="Failed to save file") -@app.get("/{paste_id}", response_class=PlainTextResponse) -async def get_file(paste_id: str, request: Request, token: Optional[str] = None): - """Get paste content or delete if token is provided""" - if not paste_id.isalnum(): - raise HTTPException(status_code=404, detail="Paste not found") +@app.get("/{file_path}", response_class=PlainTextResponse) +async def get_file(file_path: str, request: Request): + if not file_path.isalnum(): + raise HTTPException(status_code=404, detail="File not found") - file_location = UPLOAD_DIR / paste_id + file_location = UPLOAD_DIR / file_path if not file_location.exists() or not file_location.is_file(): - raise HTTPException(status_code=404, detail="Paste not found") + raise HTTPException(status_code=404, detail="File not found") try: with open(file_location, 'r', encoding='utf-8') as f: @@ -320,76 +225,10 @@ async def get_file(paste_id: str, request: Request, token: Optional[str] = None) return content except Exception as e: log("ERROR", "download_failed", - paste_id=paste_id, + paste_id=file_path, error=str(e)) raise HTTPException(status_code=500, detail="Failed to read file") - -@app.post("/{paste_id}", response_class=PlainTextResponse) -async def delete_paste_endpoint(paste_id: str, request: Request, token: Optional[str] = None): - """Delete a paste using its deletion token""" - client_ip = get_real_ip(request) - user_agent = request.headers.get("User-Agent", "unknown") - - # Validate paste_id format - if not paste_id.isalnum(): - raise HTTPException(status_code=404, detail="Paste not found") - - # Check if token is provided (query param or header) - deletion_token = token or request.headers.get("X-Delete-Token") - if not deletion_token: - log("WARNING", "deletion_failed", - paste_id=paste_id, - client_ip=client_ip, - user_agent=user_agent, - reason="missing_token") - raise HTTPException(status_code=404, detail="Not found") - - # Validate token length (prevent abuse with extremely long tokens) - if len(deletion_token) > 128: - log("WARNING", "deletion_failed", - paste_id=paste_id, - client_ip=client_ip, - user_agent=user_agent, - reason="invalid_token_length") - raise HTTPException(status_code=403, detail="Deletion failed") - - # Load metadata - metadata = load_metadata(paste_id) - if not metadata: - log("WARNING", "deletion_failed", - paste_id=paste_id, - client_ip=client_ip, - user_agent=user_agent, - reason="metadata_not_found") - raise HTTPException(status_code=404, detail="Deletion failed") - - # Verify deletion token using constant-time comparison - if not secrets.compare_digest(deletion_token, metadata.get("deletion_token", "")): - log("WARNING", "deletion_failed", - paste_id=paste_id, - client_ip=client_ip, - user_agent=user_agent, - reason="invalid_token") - raise HTTPException(status_code=403, detail="Deletion failed") - - # Delete the paste and metadata - if delete_paste(paste_id): - log("INFO", "paste_deleted", - paste_id=paste_id, - client_ip=client_ip, - user_agent=user_agent, - deletion_method="user_requested") - return "Paste deleted successfully\n" - else: - log("ERROR", "deletion_failed", - paste_id=paste_id, - client_ip=client_ip, - user_agent=user_agent, - reason="file_not_found") - raise HTTPException(status_code=500, detail="Failed to delete paste") - - @app.get("/", response_class=PlainTextResponse) async def root(): # Build authentication notice and examples if tokens are configured @@ -472,11 +311,6 @@ Invoke-RestMethod -Uri "{BASEURL}/{{path}}" # Invoke-RestMethod -Uri "{BASEURL}/{{path}}" -OutFile "filename.txt" # save to file - █ Delete: - -curl -X POST "{BASEURL}/{{path}}?token={{deletion_token}}" # delete paste - - ██ Encryption Examples with curl ██