Spaces:
Running
Running
Deploy backend from GitHub Actions
Browse files- backend/utils/validators.py +23 -18
backend/utils/validators.py
CHANGED
|
@@ -79,14 +79,16 @@ class ImageValidator:
|
|
| 79 |
|
| 80 |
# EXIF script injection check
|
| 81 |
first_2kb = content[:2048].decode('utf-8', errors='ignore').lower()
|
| 82 |
-
if any(bad in first_2kb
|
| 83 |
-
|
| 84 |
-
logger.error(
|
| 85 |
-
|
|
|
|
| 86 |
raise SecurityError("Invalid file content detected")
|
| 87 |
|
| 88 |
return ImageMetadata(
|
| 89 |
-
filename=file.filename or "unknown.png",
|
|
|
|
| 90 |
mime_type=mime, width=width, height=height, mode=mode,
|
| 91 |
content=content
|
| 92 |
)
|
|
@@ -119,28 +121,29 @@ class ImageValidator:
|
|
| 119 |
|
| 120 |
if max_channel_delta > 8.0:
|
| 121 |
raise InvalidFileError(
|
| 122 |
-
"Only grayscale chest X-ray images are supported.
|
| 123 |
-
"upload a radiograph, not a color photo."
|
| 124 |
)
|
| 125 |
|
| 126 |
if bright_ratio > 0.35 or contrast < 0.08:
|
| 127 |
raise InvalidFileError(
|
| 128 |
-
"This image does not look like a usable chest X-ray.
|
| 129 |
-
"upload a clear frontal chest radiograph."
|
| 130 |
)
|
| 131 |
|
| 132 |
try:
|
| 133 |
import cv2
|
| 134 |
|
| 135 |
cascade_path = (
|
| 136 |
-
Path(cv2.data.haarcascades)
|
| 137 |
-
"haarcascade_frontalface_default.xml"
|
| 138 |
)
|
| 139 |
if cascade_path.is_file():
|
| 140 |
face_detector = cv2.CascadeClassifier(str(cascade_path))
|
| 141 |
gray_u8 = np.uint8(gray * 255)
|
| 142 |
faces = face_detector.detectMultiScale(
|
| 143 |
-
gray_u8, scaleFactor=1.1, minNeighbors=5,
|
|
|
|
| 144 |
)
|
| 145 |
if len(faces) > 0:
|
| 146 |
raise InvalidFileError(
|
|
@@ -171,11 +174,13 @@ def sanitize_symptoms_text(text: str) -> str:
|
|
| 171 |
]
|
| 172 |
for pattern in injection_patterns:
|
| 173 |
if re.search(pattern, text, re.IGNORECASE):
|
| 174 |
-
logger.warning(
|
| 175 |
-
|
|
|
|
|
|
|
| 176 |
raise PromptInjectionError(
|
| 177 |
-
"Input contains disallowed content.
|
| 178 |
-
"naturally."
|
| 179 |
)
|
| 180 |
|
| 181 |
max_length = 2000
|
|
@@ -190,8 +195,8 @@ def validate_patient_id(patient_id: str) -> str:
|
|
| 190 |
return ""
|
| 191 |
if not re.match(r"^[a-zA-Z0-9_-]{1,50}$", patient_id):
|
| 192 |
raise ValidationError(
|
| 193 |
-
"Patient ID must be 1-50 characters:
|
| 194 |
-
"underscores only"
|
| 195 |
)
|
| 196 |
return patient_id
|
| 197 |
|
|
|
|
| 79 |
|
| 80 |
# EXIF script injection check
|
| 81 |
first_2kb = content[:2048].decode('utf-8', errors='ignore').lower()
|
| 82 |
+
if any(bad in first_2kb
|
| 83 |
+
for bad in ["<script", "javascript:", "eval("]):
|
| 84 |
+
logger.error(
|
| 85 |
+
"Security alert: Script payload detected in image bytes"
|
| 86 |
+
)
|
| 87 |
raise SecurityError("Invalid file content detected")
|
| 88 |
|
| 89 |
return ImageMetadata(
|
| 90 |
+
filename=file.filename or "unknown.png",
|
| 91 |
+
size_bytes=len(content),
|
| 92 |
mime_type=mime, width=width, height=height, mode=mode,
|
| 93 |
content=content
|
| 94 |
)
|
|
|
|
| 121 |
|
| 122 |
if max_channel_delta > 8.0:
|
| 123 |
raise InvalidFileError(
|
| 124 |
+
"Only grayscale chest X-ray images are supported. "
|
| 125 |
+
"Please upload a radiograph, not a color photo."
|
| 126 |
)
|
| 127 |
|
| 128 |
if bright_ratio > 0.35 or contrast < 0.08:
|
| 129 |
raise InvalidFileError(
|
| 130 |
+
"This image does not look like a usable chest X-ray. "
|
| 131 |
+
"Please upload a clear frontal chest radiograph."
|
| 132 |
)
|
| 133 |
|
| 134 |
try:
|
| 135 |
import cv2
|
| 136 |
|
| 137 |
cascade_path = (
|
| 138 |
+
Path(cv2.data.haarcascades)
|
| 139 |
+
/ "haarcascade_frontalface_default.xml"
|
| 140 |
)
|
| 141 |
if cascade_path.is_file():
|
| 142 |
face_detector = cv2.CascadeClassifier(str(cascade_path))
|
| 143 |
gray_u8 = np.uint8(gray * 255)
|
| 144 |
faces = face_detector.detectMultiScale(
|
| 145 |
+
gray_u8, scaleFactor=1.1, minNeighbors=5,
|
| 146 |
+
minSize=(32, 32)
|
| 147 |
)
|
| 148 |
if len(faces) > 0:
|
| 149 |
raise InvalidFileError(
|
|
|
|
| 174 |
]
|
| 175 |
for pattern in injection_patterns:
|
| 176 |
if re.search(pattern, text, re.IGNORECASE):
|
| 177 |
+
logger.warning(
|
| 178 |
+
"Prompt injection detected in input",
|
| 179 |
+
extra={"pattern": pattern}
|
| 180 |
+
)
|
| 181 |
raise PromptInjectionError(
|
| 182 |
+
"Input contains disallowed content. "
|
| 183 |
+
"Please describe symptoms naturally."
|
| 184 |
)
|
| 185 |
|
| 186 |
max_length = 2000
|
|
|
|
| 195 |
return ""
|
| 196 |
if not re.match(r"^[a-zA-Z0-9_-]{1,50}$", patient_id):
|
| 197 |
raise ValidationError(
|
| 198 |
+
"Patient ID must be 1-50 characters: "
|
| 199 |
+
"letters, numbers, hyphens, underscores only"
|
| 200 |
)
|
| 201 |
return patient_id
|
| 202 |
|