66import errno
77import os
88import re
9+ import stat
910from pathlib import Path
1011from typing import Any
1112
1516
1617_SHA256_RE = re .compile (r"^[0-9a-fA-F]{64}$" )
1718_OPEN_SUPPORTS_DIR_FD = os .open in getattr (os , "supports_dir_fd" , set ())
19+ MAX_JSON_ARTIFACT_BYTES = 4 * 1024 * 1024
1820
1921
2022def validate_latest_signal (
@@ -93,14 +95,18 @@ def _resolve_source_path(source: str, base: Path | None) -> Path:
9395
9496
9597def _read_declaration (path : Path , base : Path | None , declared_source : str ) -> bytes :
96- if getattr (os , "O_NOFOLLOW" , None ) is None or getattr (os , "O_DIRECTORY" , None ) is None :
98+ if (
99+ getattr (os , "O_NOFOLLOW" , None ) is None
100+ or getattr (os , "O_DIRECTORY" , None ) is None
101+ or getattr (os , "O_NONBLOCK" , None ) is None
102+ ):
97103 raise SignalValidationError ("secure descriptor-based source reading is unavailable" )
98104 if not _OPEN_SUPPORTS_DIR_FD :
99105 raise SignalValidationError ("secure openat source reading is unavailable" )
100106 fd = - 1
101107 try :
102108 if base is None :
103- fd = os .open (path , os .O_RDONLY | os .O_NOFOLLOW )
109+ fd = os .open (Path ( declared_source ) , os .O_RDONLY | os .O_NOFOLLOW | getattr ( os , "O_NONBLOCK" , 0 ) )
104110 else :
105111 raw_path = Path (declared_source )
106112 try :
@@ -117,11 +123,18 @@ def _read_declaration(path: Path, base: Path | None, declared_source: str) -> by
117123 fd = next_fd
118124 final_fd = - 1
119125 try :
120- final_fd = os .open (parts [- 1 ], os .O_RDONLY | os .O_NOFOLLOW , dir_fd = fd )
126+ final_fd = os .open (
127+ parts [- 1 ], os .O_RDONLY | os .O_NOFOLLOW | getattr (os , "O_NONBLOCK" , 0 ), dir_fd = fd
128+ )
121129 finally :
122130 parent_fd = fd
123131 fd = final_fd
124132 os .close (parent_fd )
133+ metadata = os .fstat (fd )
134+ if not stat .S_ISREG (metadata .st_mode ):
135+ raise SignalValidationError ("declaration source must be a regular file" )
136+ if metadata .st_size > MAX_JSON_ARTIFACT_BYTES :
137+ raise SignalValidationError ("declaration source exceeds maximum size" )
125138 with os .fdopen (fd , "rb" ) as stream :
126139 fd = - 1
127140 return stream .read ()
@@ -181,5 +194,5 @@ def _parse_datetime(value: Any, name: str) -> dt.datetime:
181194 except ValueError as exc :
182195 raise SignalValidationError (f"{ name } must be an ISO datetime" ) from exc
183196 if parsed .tzinfo is None :
184- return parsed . replace ( tzinfo = dt . timezone . utc )
197+ raise SignalValidationError ( f" { name } must be an ISO datetime with an explicit timezone" )
185198 return parsed .astimezone (dt .timezone .utc )
0 commit comments