Anish530 commited on
Commit
96d596c
·
1 Parent(s): 47f3442

Added Secure Video Upload Validation

Browse files
backend/app/security/video_validator.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import magic
3
+ import cv2
4
+ import tempfile
5
+ from fastapi import UploadFile
6
+ from typing import Tuple
7
+ from app.core.logger import logger
8
+
9
+ class VideoValidator:
10
+ ALLOWED_MIMES = {"video/mp4", "video/quicktime", "video/x-msvideo", "video/webm"}
11
+ ALLOWED_EXTENSIONS = {".mp4", ".mov", ".avi", ".webm"}
12
+ MAX_FILE_BYTES = 100 * 1024 * 1024 # 100 MB Limit
13
+
14
+ @classmethod
15
+ def validate_stream(cls, file: UploadFile) -> Tuple[bool, str]:
16
+ try:
17
+ ext = os.path.splitext(file.filename)[1].lower()
18
+ if ext not in cls.ALLOWED_EXTENSIONS:
19
+ logger.warning(f"Security Alert: Blocked illegal file extension '{ext}'")
20
+ return False, f"File extension {ext} is not allowed."
21
+
22
+ file_head = file.file.read(2048)
23
+ file.file.seek(0)
24
+
25
+ actual_mime = magic.from_buffer(file_head, mime=True)
26
+ if actual_mime not in cls.ALLOWED_MIMES:
27
+ logger.warning(f"Security Alert: Blocked illegal MIME type '{actual_mime}' masquerading as {ext}")
28
+ return False, f"Invalid file signature detected: {actual_mime}"
29
+
30
+ bytes_read = 0
31
+
32
+ with tempfile.NamedTemporaryFile(delete=True, suffix=ext) as temp_video:
33
+ while chunk := file.file.read(1024 * 1024): # Read 1 MB at a time
34
+ bytes_read += len(chunk)
35
+ if bytes_read > cls.MAX_FILE_BYTES:
36
+ logger.warning(f"Security Alert: Blocked upload exceeding {cls.MAX_FILE_BYTES} bytes")
37
+ return False, "File size exceeds the maximum allowed limit"
38
+
39
+ temp_video.write(chunk)
40
+
41
+ temp_video.flush()
42
+ file.file.seek(0)
43
+
44
+ cap = cv2.VideoCapture(temp_video.name)
45
+
46
+ if not cap.isOpened():
47
+ logger.warning("Security Alert: OpenCV failed to open the video container. File may be corrupted or disguised.")
48
+ cap.release()
49
+ return False, "Video container is corrupted or unreadable."
50
+
51
+ success, frame = cap.read()
52
+ cap.release()
53
+
54
+ if not success or frame is None:
55
+ logger.warning("Security Alert: OpenCV opened container but failed to extract any visual frames.")
56
+ return False, "Video contains no readable visual frames."
57
+
58
+ return True, "Valid"
59
+
60
+ except Exception as e:
61
+ logger.error(f"Critical error during video validation: {str(e)}")
62
+ return False, "An unexpected error occured while processing the video stream."
backend/app/utils/file_handler.py CHANGED
@@ -4,6 +4,7 @@ import uuid
4
  import magic
5
  from PIL import Image, UnidentifiedImageError
6
  from fastapi import UploadFile, HTTPException
 
7
 
8
 
9
  UPLOAD_DIR = "uploads"
@@ -17,29 +18,37 @@ ALLOWED_TYPES = {
17
  }
18
 
19
  def validate_file(file: UploadFile):
 
 
 
 
 
 
 
 
 
 
20
  file_bytes = file.file.read()
21
  size = len(file_bytes)
 
22
  if size > MAX_FILE_SIZE:
23
  raise HTTPException(status_code=400, detail="File size too large")
24
-
25
  actual_mime = magic.from_buffer(file_bytes, mime=True)
26
- if actual_mime not in ALLOWED_TYPES:
27
- raise HTTPException(status_code=400, detail=f"Invalid file signature. Detected: {actual_mime}")
28
-
29
- if "image" in actual_mime:
30
- try:
31
- file.file.seek(0)
32
- with Image.open(file.file) as img:
33
- img.verify()
34
- width, height = img.size
35
-
36
- if width > 8000 or height > 8000:
37
- raise HTTPException(status_code=400, detail="Image resolution exceeds 8000x8000 limit")
38
- except UnidentifiedImageError:
39
- raise HTTPException(status_code=400, detail="Image file is corrupted or unreadable")
40
- except Image.DecompressionBombError:
41
- raise HTTPException(status_code=400, detail="Decompression Bomb detected! Image rejected")
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  file.file.seek(0)
44
  return size
45
 
 
4
  import magic
5
  from PIL import Image, UnidentifiedImageError
6
  from fastapi import UploadFile, HTTPException
7
+ from app.security.video_validator import VideoValidator
8
 
9
 
10
  UPLOAD_DIR = "uploads"
 
18
  }
19
 
20
  def validate_file(file: UploadFile):
21
+ ext = file.filename.split(".")[-1].lower() if "." in file.filename else ""
22
+
23
+ if ext in ["mp4", "mov", "avi", "webm"]:
24
+ is_valid, validation_reason = VideoValidator.validate_stream(file)
25
+
26
+ if not is_valid:
27
+ raise HTTPException(status_code=400, detail=validation_reason)
28
+
29
+ return file.size or 0
30
+
31
  file_bytes = file.file.read()
32
  size = len(file_bytes)
33
+
34
  if size > MAX_FILE_SIZE:
35
  raise HTTPException(status_code=400, detail="File size too large")
36
+
37
  actual_mime = magic.from_buffer(file_bytes, mime=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
+ if "image" not in actual_mime:
40
+ raise HTTPException(status_code=400, detail=f"Invalid image signature. Detected: {actual_mime}")
41
+
42
+ try:
43
+ file.file.seek(0)
44
+ with Image.open(file.file) as img:
45
+ img.verify()
46
+ width, height = img.size
47
+ if width > 8000 or height > 8000:
48
+ raise HTTPException(status_code=400, detail="Image resolution exceeds 8000x8000 limit.")
49
+ except Image.DecompressionBombError:
50
+ raise HTTPException(status_code=400, detail="Decompression Bomb detected! Image rejected")
51
+
52
  file.file.seek(0)
53
  return size
54
 
backend/main.py CHANGED
@@ -2,7 +2,6 @@ from fastapi import FastAPI
2
  from contextlib import asynccontextmanager
3
  from slowapi import _rate_limit_exceeded_handler
4
  from slowapi.errors import RateLimitExceeded
5
- from app.db.init_db import init_db
6
  from app.api.user_routes import router as user_router
7
  from app.api.auth_routes import router as auth_router
8
  from app.api.profile_routes import router as profile_router
@@ -17,7 +16,7 @@ from app.middleware.security_headers import SecurityHeadersMiddleware
17
 
18
  @asynccontextmanager
19
  async def lifespan(app: FastAPI):
20
- init_db()
21
  yield
22
 
23
  app = FastAPI(
 
2
  from contextlib import asynccontextmanager
3
  from slowapi import _rate_limit_exceeded_handler
4
  from slowapi.errors import RateLimitExceeded
 
5
  from app.api.user_routes import router as user_router
6
  from app.api.auth_routes import router as auth_router
7
  from app.api.profile_routes import router as profile_router
 
16
 
17
  @asynccontextmanager
18
  async def lifespan(app: FastAPI):
19
+ pass
20
  yield
21
 
22
  app = FastAPI(
backend/requirements.txt CHANGED
Binary files a/backend/requirements.txt and b/backend/requirements.txt differ
 
fake_video.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fd7a2612f459072921f03279a0e61ea7fb923a1231291632546fae6c4079ec5e
3
+ size 15