Compare commits
No commits in common. "c7446d0f4806c90da23a8f6efe880e32fc18e022" and "ed081b280cab76845a43118d33fc6782e72e7cfc" have entirely different histories.
c7446d0f48
...
ed081b280c
2 changed files with 9 additions and 180 deletions
|
|
@ -68,11 +68,6 @@ Invoke-RestMethod -Uri "https://linedump.com/{path}"
|
||||||
Invoke-RestMethod -Uri "https://linedump.com/{path}" -OutFile "filename.txt" # save to file
|
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 ██
|
██ Encryption Examples with curl ██
|
||||||
|
|
||||||
|
|
@ -146,7 +141,7 @@ Invoke-RestMethod -Uri "https://linedump.com/" -Headers @{"Authorization"="Beare
|
||||||
|
|
||||||
**Simple / Testing**
|
**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`
|
Open `http://127.0.0.1:8000`
|
||||||
|
|
||||||
|
|
|
||||||
182
main.py
182
main.py
|
|
@ -107,92 +107,6 @@ def generate_random_path(length: int = None) -> str:
|
||||||
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
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:
|
def validate_upload_token(request: Request) -> bool:
|
||||||
"""Validate upload token if authentication is enabled"""
|
"""Validate upload token if authentication is enabled"""
|
||||||
if not UPLOAD_TOKENS:
|
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
|
file_path = UPLOAD_DIR / random_path
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Generate deletion token
|
|
||||||
deletion_token = generate_deletion_token()
|
|
||||||
|
|
||||||
# Save paste content
|
|
||||||
with open(file_path, 'w', encoding='utf-8') as f:
|
with open(file_path, 'w', encoding='utf-8') as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
# Save metadata with deletion token
|
|
||||||
save_metadata(random_path, deletion_token, client_ip)
|
|
||||||
|
|
||||||
log("INFO", "paste_created",
|
log("INFO", "paste_created",
|
||||||
paste_id=random_path,
|
paste_id=random_path,
|
||||||
client_ip=client_ip,
|
client_ip=client_ip,
|
||||||
user_agent=user_agent,
|
user_agent=user_agent,
|
||||||
size_bytes=len(content))
|
size_bytes=len(content))
|
||||||
|
|
||||||
# Return URL and deletion token
|
return f"{BASEURL}/{random_path}\n"
|
||||||
return f"{BASEURL}/{random_path}\nDelete with HTTP POST: {BASEURL}/{random_path}?token={deletion_token}\n"
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log("ERROR", "upload_failed",
|
log("ERROR", "upload_failed",
|
||||||
|
|
@ -302,16 +208,15 @@ async def upload_text(request: Request, authorized: bool = Depends(validate_uplo
|
||||||
error=str(e))
|
error=str(e))
|
||||||
raise HTTPException(status_code=500, detail="Failed to save file")
|
raise HTTPException(status_code=500, detail="Failed to save file")
|
||||||
|
|
||||||
@app.get("/{paste_id}", response_class=PlainTextResponse)
|
@app.get("/{file_path}", response_class=PlainTextResponse)
|
||||||
async def get_file(paste_id: str, request: Request, token: Optional[str] = None):
|
async def get_file(file_path: str, request: Request):
|
||||||
"""Get paste content or delete if token is provided"""
|
if not file_path.isalnum():
|
||||||
if not paste_id.isalnum():
|
raise HTTPException(status_code=404, detail="File not found")
|
||||||
raise HTTPException(status_code=404, detail="Paste 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():
|
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:
|
try:
|
||||||
with open(file_location, 'r', encoding='utf-8') as f:
|
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
|
return content
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log("ERROR", "download_failed",
|
log("ERROR", "download_failed",
|
||||||
paste_id=paste_id,
|
paste_id=file_path,
|
||||||
error=str(e))
|
error=str(e))
|
||||||
raise HTTPException(status_code=500, detail="Failed to read file")
|
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)
|
@app.get("/", response_class=PlainTextResponse)
|
||||||
async def root():
|
async def root():
|
||||||
# Build authentication notice and examples if tokens are configured
|
# 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
|
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 ██
|
██ Encryption Examples with curl ██
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue