返回提交历史
Modified
g4f/Provider/search/CachedSearch.py
+5
-1
Modified
g4f/api/__init__.py
+36
-23
Modified
g4f/gui/server/backend_api.py
+27
-17
Modified
g4f/gui/server/website.py
+10
-8
Modified
g4f/image/copy_images.py
+18
-8
Modified
g4f/mcp/server.py
+14
-7
Modified
g4f/requests/cdp.py
+9
-1
XFEstudio/gpt4free
fix(security): resolve path injection vulnerabilities with realpath canonicalization and jail checks
21382975
代码差异
7 个文件
+119
-65
@@ -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():
@@ -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():
@@ -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":
@@ -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):
@@ -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:
@@ -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")
@@ -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