XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 1
返回提交历史

XFEstudio/gpt4free

fix(security): resolve path injection vulnerabilities with realpath canonicalization and jail checks

21382975
Anand Mall <anand@example.com>
提交于

代码差异

7 个文件 +119 -65
Modified g4f/Provider/search/CachedSearch.py +5 -1
@@ -94,7 +94,11 @@ class CachedSearch(AsyncGeneratorProvider, AuthFileMixin):
94 94 Path(get_cookies_dir()) / ".scrape_cache" / "web_search" / f"{date.today()}"
95 95 )
96 96 cache_dir.mkdir(parents=True, exist_ok=True)
97 cache_file = cache_dir / f"{quote_plus(prompt[:20])}.{md5_hash}.cache"
97 safe_prompt = "".join(c for c in prompt[:20] if c.isalnum() or c in ("-", "_"))
98 filename = f"{safe_prompt}_{md5_hash}.cache" if safe_prompt else f"{md5_hash}.cache"
99 cache_file = (cache_dir / filename).resolve()
100 if not str(cache_file).startswith(str(cache_dir.resolve())):
101 cache_file = (cache_dir / f"{md5_hash}.cache").resolve()
98 102
99 103 search_results: Optional[SearchResults] = None
100 104 if cache_file.exists():
Modified g4f/api/__init__.py +36 -23
@@ -2204,29 +2204,43 @@ class Api:
2204 2204 else:
2205 2205 return 0
2206 2206
2207 target = os.path.join(get_media_dir(), os.path.basename(filename))
2207 media_dir = os.path.realpath(get_media_dir())
2208 clean_filename = secure_filename(os.path.basename(filename))
2209 if not clean_filename:
2210 return ErrorResponse.from_message("Invalid file name", HTTP_400_BAD_REQUEST)
2211
2212 target = os.path.realpath(os.path.join(media_dir, clean_filename))
2213 if not target.startswith(media_dir + os.sep):
2214 return ErrorResponse.from_message("Access denied", HTTP_403_FORBIDDEN)
2215
2216 thumbnail_path = None
2208 2217 if thumbnail and has_pillow:
2209 thumbnail_dir = os.path.join(get_media_dir(), "thumbnails")
2210 thumbnail = os.path.join(thumbnail_dir, filename)
2218 thumbnail_dir = os.path.realpath(os.path.join(media_dir, "thumbnails"))
2219 os.makedirs(thumbnail_dir, exist_ok=True)
2220 cand_thumb = os.path.realpath(os.path.join(thumbnail_dir, clean_filename))
2221 if cand_thumb.startswith(thumbnail_dir + os.sep):
2222 thumbnail_path = cand_thumb
2223
2211 2224 if not os.path.isfile(target):
2212 other_name = os.path.join(
2213 get_media_dir(), os.path.basename(quote_plus(filename))
2214 )
2215 if os.path.isfile(other_name):
2216 target = other_name
2217 ext = os.path.splitext(filename)[1][1:]
2225 decoded_name = secure_filename(os.path.basename(unquote_plus(filename)))
2226 if decoded_name:
2227 cand_other = os.path.realpath(os.path.join(media_dir, decoded_name))
2228 if cand_other.startswith(media_dir + os.sep) and os.path.isfile(cand_other):
2229 target = cand_other
2230
2231 ext = os.path.splitext(clean_filename)[1][1:]
2218 2232 mime_type = EXTENSIONS_MAP.get(ext)
2219 2233 stat_result = SimpleNamespace()
2220 2234 stat_result.st_size = 0
2221 stat_result.st_mtime = get_timestamp(filename)
2222 if thumbnail and has_pillow and os.path.isfile(thumbnail):
2223 stat_result.st_size = os.stat(thumbnail).st_size
2235 stat_result.st_mtime = get_timestamp(clean_filename)
2236 if thumbnail and has_pillow and thumbnail_path and os.path.isfile(thumbnail_path):
2237 stat_result.st_size = os.stat(thumbnail_path).st_size
2224 2238 elif not thumbnail and os.path.isfile(target):
2225 2239 stat_result.st_size = os.stat(target).st_size
2226 2240 headers = {
2227 2241 "cache-control": "public, max-age=31536000",
2228 2242 "last-modified": formatdate(stat_result.st_mtime, usegmt=True),
2229 "etag": f'"{hashlib.md5(filename.encode()).hexdigest()}"',
2243 "etag": f'"{hashlib.md5(clean_filename.encode()).hexdigest()}"',
2230 2244 **(
2231 2245 {
2232 2246 "content-length": str(stat_result.st_size),
@@ -2245,7 +2259,7 @@ class Api:
2245 2259 response = FileResponse(
2246 2260 target,
2247 2261 headers=headers,
2248 filename=filename,
2262 filename=clean_filename,
2249 2263 )
2250 2264 try:
2251 2265 if_none_match = request.headers["if-none-match"]
@@ -2260,7 +2274,7 @@ class Api:
2260 2274 if source_url is None:
2261 2275 backend_url = os.environ.get("G4F_BACKEND_URL")
2262 2276 if backend_url:
2263 source_url = f"{backend_url}/media/{filename}"
2277 source_url = f"{backend_url}/media/{clean_filename}"
2264 2278 ssl = False
2265 2279 if source_url is not None:
2266 2280 if not is_safe_url(source_url):
@@ -2272,20 +2286,19 @@ class Api:
2272 2286 debug.error(f"Download failed: {source_url}")
2273 2287 debug.error(e)
2274 2288 return ErrorResponse.from_message("Failed to fetch remote media", HTTP_502_BAD_GATEWAY)
2275 if thumbnail and has_pillow:
2289 if thumbnail and has_pillow and thumbnail_path:
2276 2290 try:
2277 if not os.path.isfile(thumbnail):
2291 if not os.path.isfile(thumbnail_path) and os.path.isfile(target):
2278 2292 image = Image.open(target)
2279 os.makedirs(thumbnail_dir, exist_ok=True)
2280 process_image(image, save=thumbnail)
2281 debug.log(f"Thumbnail created: {thumbnail}")
2293 process_image(image, save=thumbnail_path)
2294 debug.log(f"Thumbnail created: {thumbnail_path}")
2282 2295 except Exception as e:
2283 2296 logger.exception(e)
2284 if thumbnail and os.path.isfile(thumbnail):
2285 result = thumbnail
2297 if thumbnail and has_pillow and thumbnail_path and os.path.isfile(thumbnail_path):
2298 result = thumbnail_path
2286 2299 else:
2287 2300 result = target
2288 if not os.path.isfile(result):
2301 if not os.path.isfile(result) or not result.startswith(media_dir + os.sep):
2289 2302 return ErrorResponse.from_message("File not found", HTTP_404_NOT_FOUND)
2290 2303
2291 2304 async def stream():
Modified g4f/gui/server/backend_api.py +27 -17
@@ -402,13 +402,16 @@ class Backend_Api(Api):
402 402 if not _DATE_RE.match(date):
403 403 return (jsonify({"error": {"message": "Invalid date format"}}), 400)
404 404 try:
405 datetime.date.fromisoformat(date)
405 safe_date = datetime.date.fromisoformat(date).isoformat()
406 406 except ValueError:
407 407 return (jsonify({"error": {"message": "Invalid date"}}), 400)
408 cache_dir = Path(get_cookies_dir()) / ".usage"
409 cache_file = cache_dir / f"{date}.jsonl"
410 if cache_file.exists():
411 return Response(cache_file.read_text(), mimetype="text/plain")
408 real_dir = os.path.realpath(str(Path(get_cookies_dir()) / ".usage"))
409 target = os.path.realpath(os.path.join(real_dir, f"{safe_date}.jsonl"))
410 if not target.startswith(real_dir + os.sep):
411 return (jsonify({"error": {"message": "Invalid date"}}), 400)
412 target_path = Path(target)
413 if target_path.exists():
414 return Response(target_path.read_text(), mimetype="text/plain")
412 415 else:
413 416 return (
414 417 jsonify(
@@ -734,10 +737,13 @@ class Backend_Api(Api):
734 737 + json.dumps(parameters, sort_keys=True).encode()
735 738 ).hexdigest()
736 739 cache_dir = Path(get_cookies_dir()) / ".scrape_cache" / "create"
737 cache_file = (
738 cache_dir
739 / f"{quote_plus(request.args.get('prompt', '').strip()[:20])}.{cache_id}.txt"
740 )
740 safe_prompt = secure_filename(request.args.get("prompt", "").strip()[:20])
741 file_name = f"{safe_prompt}_{cache_id}.txt" if safe_prompt else f"{cache_id}.txt"
742 real_cache_dir = os.path.realpath(str(cache_dir))
743 target = os.path.realpath(os.path.join(real_cache_dir, file_name))
744 if not target.startswith(real_cache_dir + os.sep):
745 target = os.path.realpath(os.path.join(real_cache_dir, f"{cache_id}.txt"))
746 cache_file = Path(target)
741 747 response = None
742 748 if cache_file.exists():
743 749 with cache_file.open("r") as f:
@@ -756,15 +762,19 @@ class Backend_Api(Api):
756 762 response = cast_str(iter_run_tools(provider_handler, **parameters))
757 763 if isinstance(response, str) and "\n" not in response:
758 764 if response.startswith("/media/"):
759 media_dir = get_media_dir()
760 filename = os.path.basename(response.split("?")[0])
765 media_dir = os.path.realpath(get_media_dir())
766 filename = secure_filename(os.path.basename(response.split("?")[0]))
767 target_file = os.path.realpath(os.path.join(media_dir, filename))
768 if not target_file.startswith(media_dir + os.sep):
769 return jsonify({"error": {"message": "Invalid file"}}), 400
761 770 if not cache_id:
762 771 try:
763 772 return send_from_directory(
764 os.path.abspath(media_dir), filename
773 media_dir, filename
765 774 )
766 775 finally:
767 os.remove(os.path.join(media_dir, filename))
776 if os.path.exists(target_file):
777 os.remove(target_file)
768 778 else:
769 779 if response.startswith("/") and not response.startswith("//"):
770 780 return redirect(response)
@@ -1006,12 +1016,12 @@ class Backend_Api(Api):
1006 1016 file
1007 1017 and (file.filename.endswith(".json") or file.filename.endswith(".har"))
1008 1018 ):
1009 filename = secure_filename(file.filename)
1019 filename = secure_filename(os.path.basename(file.filename))
1010 1020 if not filename:
1011 1021 return "Not supported file", 400
1012 cookies_dir = os.path.abspath(get_cookies_dir())
1013 target_path = os.path.abspath(os.path.join(cookies_dir, filename))
1014 if not target_path.startswith(cookies_dir):
1022 cookies_dir = os.path.realpath(get_cookies_dir())
1023 target_path = os.path.realpath(os.path.join(cookies_dir, filename))
1024 if not target_path.startswith(cookies_dir + os.sep):
1015 1025 return "Forbidden file path", 403
1016 1026 file.save(target_path)
1017 1027 if hasattr(os, "chmod") and os.name != "nt":
Modified g4f/gui/server/website.py +10 -8
@@ -57,14 +57,16 @@ def render(filename="home", download_url: str = GITHUB_URL):
57 57 latest_version = version.utils.current_version
58 58 today = datetime.today().strftime("%Y-%m-%d")
59 59 cache_dir = os.path.join(get_cookies_dir(), ".gui_cache", today)
60 if not request.args.get("g4f_session"):
61 latest_version = str(latest_version) + quote(
62 unquote(request.query_string.decode())
63 )
64 cache_file = os.path.join(
65 cache_dir,
66 f"{secure_filename(f'{version.utils.current_version}-{latest_version}')}.{secure_filename(filename)}",
67 )
60 qs_suffix = ""
61 if not request.args.get("g4f_session") and request.query_string:
62 qs_suffix = "_" + hashlib.md5(request.query_string).hexdigest()[:8]
63 safe_filename = secure_filename(os.path.basename(filename))
64 safe_prefix = secure_filename(f"{version.utils.current_version}-{latest_version}")
65 cache_file_name = f"{safe_prefix}{qs_suffix}.{safe_filename}"
66 real_cache_dir = os.path.realpath(cache_dir)
67 cache_file = os.path.realpath(os.path.join(cache_dir, cache_file_name))
68 if not cache_file.startswith(real_cache_dir + os.sep):
69 raise ValueError("Invalid cache path")
68 70 if os.path.isfile(cache_file + ".js"):
69 71 cache_file += ".js"
70 72 if not os.path.exists(cache_file):
Modified g4f/image/copy_images.py +18 -8
@@ -181,17 +181,28 @@ async def copy_media(
181 181 if image is None or image.startswith("/"):
182 182 return image
183 183
184 target_path = target
184 real_dest_dir = os.path.realpath(dest_dir)
185
186 def _safe_target(name: str) -> str:
187 clean = secure_filename(os.path.basename(name))
188 if not clean:
189 clean = hashlib.md5(name.encode()).hexdigest()
190 cand = os.path.realpath(os.path.join(real_dest_dir, clean))
191 if not cand.startswith(real_dest_dir + os.sep):
192 raise ValueError("Unsafe media target path")
193 return cand
194
195 target_path = _safe_target(target) if target else None
185 196 media_extension = ""
186 197
187 198 if target_path is None:
188 199 media_extension = get_media_extension(image)
189 200 path = urlparse(image).path
190 201 if path.startswith("/media/"):
191 filename = secure_filename(path[len("/media/") :])
202 filename = secure_filename(os.path.basename(path[len("/media/") :]))
192 203 else:
193 204 filename = get_filename(tags, alt, media_extension, image)
194 target_path = os.path.join(dest_dir, filename)
205 target_path = _safe_target(filename)
195 206
196 207 try:
197 208 if image.startswith("data:"):
@@ -208,7 +219,7 @@ async def copy_media(
208 219 response.raise_for_status()
209 220 if target is None:
210 221 filename = update_filename(response, filename)
211 target_path = os.path.join(dest_dir, filename)
222 target_path = _safe_target(filename)
212 223 media_type = response.headers.get(
213 224 "content-type", "application/octet-stream"
214 225 )
@@ -222,7 +233,7 @@ async def copy_media(
222 233 )
223 234 if target is None and not media_extension:
224 235 media_extension = f".{MEDIA_TYPE_MAP[media_type]}"
225 target_path = f"{target_path}{media_extension}"
236 target_path = _safe_target(f"{os.path.basename(target_path)}{media_extension}")
226 237 with open(target_path, "wb") as f:
227 238 async for chunk in response.content.iter_any():
228 239 f.write(chunk)
@@ -233,9 +244,8 @@ async def copy_media(
233 244 file_header = f.read(12)
234 245 try:
235 246 detected_type = is_accepted_format(file_header)
236 media_extension = f".{detected_type.split('/')[-1]}"
237 media_extension = media_extension.replace("jpeg", "jpg")
238 new_path = f"{target_path}{media_extension}"
247 media_extension = f".{detected_type.split('/')[-1]}".replace("jpeg", "jpg")
248 new_path = _safe_target(f"{os.path.basename(target_path)}{media_extension}")
239 249 os.rename(target_path, new_path)
240 250 target_path = new_path
241 251 except ValueError:
Modified g4f/mcp/server.py +14 -7
@@ -334,15 +334,22 @@ class MCPServer:
334 334 m = re.match("^[0-9]+", s)
335 335 return int(m.group(0)) if m else 0
336 336
337 target = os.path.join(get_media_dir(), os.path.basename(filename))
337 media_dir = os.path.realpath(get_media_dir())
338 clean_name = secure_filename(os.path.basename(filename))
339 if not clean_name:
340 return web.Response(status=404, text="File not found")
341
342 target = os.path.realpath(os.path.join(media_dir, clean_name))
343 if not target.startswith(media_dir + os.sep):
344 return web.Response(status=403, text="Access denied")
338 345
339 346 # Try URL-decoded filename if not found
340 347 if not os.path.isfile(target):
341 other_name = os.path.join(
342 get_media_dir(), os.path.basename(unquote_plus(filename))
343 )
344 if os.path.isfile(other_name):
345 target = other_name
348 decoded_name = secure_filename(os.path.basename(unquote_plus(filename)))
349 if decoded_name:
350 candidate = os.path.realpath(os.path.join(media_dir, decoded_name))
351 if candidate.startswith(media_dir + os.sep) and os.path.isfile(candidate):
352 target = candidate
346 353
347 354 # Get file extension and mime type
348 355 ext = os.path.splitext(filename)[1][1:].lower()
@@ -358,7 +365,7 @@ class MCPServer:
358 365 sys.stderr.write(f"File copied from {source_url}\n")
359 366 except Exception as e:
360 367 sys.stderr.write(f"Download failed: {source_url} - {e}\n")
361 raise web.HTTPFound(location=source_url)
368 return web.Response(status=404, text="File not found")
362 369
363 370 if not os.path.isfile(target):
364 371 return web.Response(status=404, text="File not found")
Modified g4f/requests/cdp.py +9 -1
@@ -806,7 +806,15 @@ class CDPSession:
806 806 datekey = datetime.date.today().isoformat()
807 807 screenshot_dir = get_screenshot_dir(datekey)
808 808 # Use original URL for filename to distinguish between similar URLs
809 filepath = os.path.join(screenshot_dir, f"{secure_filename(url_without_suffix.replace('https://', '').replace('http://', '').replace('www.', ''))}{'.webp' if n == 1 else f'_{n}.webp'}")
809 base_name = secure_filename(url_without_suffix.replace('https://', '').replace('http://', '').replace('www.', ''))
810 base_name = os.path.basename(base_name)
811 if not base_name:
812 base_name = hashlib.md5(url.encode()).hexdigest()
813 filename = f"{base_name}{'.webp' if n == 1 else f'_{n}.webp'}"
814 real_root = os.path.realpath(screenshot_dir)
815 filepath = os.path.realpath(os.path.join(screenshot_dir, filename))
816 if not filepath.startswith(real_root + os.sep):
817 raise ValueError("Unsafe screenshot path")
810 818 if os.path.exists(filepath):
811 819 debug.log(f"Screenshot already exists: {filepath}")
812 820 return filepath