CRITICAL: fix possible path traversal

This commit is contained in:
Domingo Dirutigliano 2026-04-10 14:52:22 +02:00
parent 16f96aa6f6
commit 16d0bfd169
No known key found for this signature in database
2 changed files with 33 additions and 7 deletions

View File

@ -6,13 +6,14 @@ import psutil
import sys import sys
import nftables import nftables
from socketio import AsyncServer from socketio import AsyncServer
from fastapi import Path from typing import Annotated, List, Union
from typing import Annotated
from functools import wraps from functools import wraps
from pydantic import BaseModel, ValidationError from pydantic import BaseModel, ValidationError, Field
import traceback import traceback
from utils.models import StatusMessageModel from utils.models import StatusMessageModel
from typing import List from pathlib import Path
from fastapi import HTTPException, status
LOCALHOST_IP = socket.gethostbyname(os.getenv("LOCALHOST_IP","127.0.0.1")) LOCALHOST_IP = socket.gethostbyname(os.getenv("LOCALHOST_IP","127.0.0.1"))
@ -31,7 +32,32 @@ FIREGEX_SOCKET = os.path.join(FIREGEX_SOCKET_DIR, "firegex.sock") if FIREGEX_SOC
JWT_ALGORITHM: str = "HS256" JWT_ALGORITHM: str = "HS256"
API_VERSION = "{{VERSION_PLACEHOLDER}}" if "{" not in "{{VERSION_PLACEHOLDER}}" else "0.0.0" API_VERSION = "{{VERSION_PLACEHOLDER}}" if "{" not in "{{VERSION_PLACEHOLDER}}" else "0.0.0"
PortType = Annotated[int, Path(gt=0, lt=65536)] PortType = Annotated[int, Field(gt=0, lt=65536)]
def safe_join(base_dir: Union[str, Path], *paths: str) -> Path:
"""
Safely join a base directory with one or more path components.
Returns the resolved Path object if safe.
Raises HTTPException 403 if a directory traversal attack is detected.
"""
# 1. Convert base_dir to a resolved, absolute Path
base_path = Path(base_dir).resolve()
# 2. Join the parts and resolve the result (removes symbols like '../')
# If the user input is an absolute path (starting with /),
# joinpath handles it correctly or we can strip leading slashes.
clean_paths = [p.lstrip("/") for p in paths]
target_path = base_path.joinpath(*clean_paths).resolve()
# 3. Security check
if not target_path.is_relative_to(base_path):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied: Path is invalid."
)
return target_path
async def run_func(func, *args, **kwargs): async def run_func(func, *args, **kwargs):
if asyncio.iscoroutinefunction(func): if asyncio.iscoroutinefunction(func):

View File

@ -5,7 +5,7 @@ from typing import Callable
from fastapi import APIRouter from fastapi import APIRouter
from starlette.responses import StreamingResponse from starlette.responses import StreamingResponse
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from utils import DEBUG, ON_DOCKER, ROUTERS_DIR, list_files, run_func from utils import DEBUG, ON_DOCKER, ROUTERS_DIR, list_files, run_func, safe_join
from utils.models import ResetRequest from utils.models import ResetRequest
import asyncio import asyncio
import traceback import traceback
@ -20,7 +20,7 @@ async def frontend_debug_proxy(path):
return StreamingResponse(resp.aiter_bytes(),status_code=resp.status_code, headers=resp.headers) return StreamingResponse(resp.aiter_bytes(),status_code=resp.status_code, headers=resp.headers)
async def react_deploy(path): async def react_deploy(path):
file_request = os.path.join(REACT_BUILD_DIR, path) file_request = safe_join(REACT_BUILD_DIR, path)
if not os.path.isfile(file_request): if not os.path.isfile(file_request):
return FileResponse(REACT_HTML_PATH, media_type='text/html') return FileResponse(REACT_HTML_PATH, media_type='text/html')
else: else: