Enhance security checks during model extraction, add Timeout and improve error handling
This commit is contained in:
parent
ae056472c8
commit
918354edc7
82
addon.py
82
addon.py
@ -14,7 +14,7 @@ import shutil
|
|||||||
import zipfile
|
import zipfile
|
||||||
from bpy.props import StringProperty, IntProperty, BoolProperty, EnumProperty
|
from bpy.props import StringProperty, IntProperty, BoolProperty, EnumProperty
|
||||||
import io
|
import io
|
||||||
from contextlib import redirect_stdout
|
from contextlib import redirect_stdout, suppress
|
||||||
|
|
||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "Blender MCP",
|
"name": "Blender MCP",
|
||||||
@ -716,10 +716,8 @@ class BlenderMCPServer:
|
|||||||
return {"error": f"Failed to import model: {str(e)}"}
|
return {"error": f"Failed to import model: {str(e)}"}
|
||||||
finally:
|
finally:
|
||||||
# Clean up temporary directory
|
# Clean up temporary directory
|
||||||
try:
|
with suppress(Exception):
|
||||||
shutil.rmtree(temp_dir)
|
shutil.rmtree(temp_dir)
|
||||||
except:
|
|
||||||
print(f"Failed to clean up temporary directory: {temp_dir}")
|
|
||||||
else:
|
else:
|
||||||
return {"error": f"Requested format or resolution not available for this model"}
|
return {"error": f"Requested format or resolution not available for this model"}
|
||||||
|
|
||||||
@ -1398,7 +1396,8 @@ class BlenderMCPServer:
|
|||||||
|
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
"https://api.sketchfab.com/v3/me",
|
"https://api.sketchfab.com/v3/me",
|
||||||
headers=headers
|
headers=headers,
|
||||||
|
timeout=30 # Add timeout of 30 seconds
|
||||||
)
|
)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
@ -1413,6 +1412,11 @@ class BlenderMCPServer:
|
|||||||
"enabled": False,
|
"enabled": False,
|
||||||
"message": f"Sketchfab API key seems invalid. Status code: {response.status_code}"
|
"message": f"Sketchfab API key seems invalid. Status code: {response.status_code}"
|
||||||
}
|
}
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
return {
|
||||||
|
"enabled": False,
|
||||||
|
"message": f"Timeout connecting to Sketchfab API. Check your internet connection."
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {
|
return {
|
||||||
"enabled": False,
|
"enabled": False,
|
||||||
@ -1470,11 +1474,12 @@ class BlenderMCPServer:
|
|||||||
response = requests.get(
|
response = requests.get(
|
||||||
"https://api.sketchfab.com/v3/search",
|
"https://api.sketchfab.com/v3/search",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
params=params
|
params=params,
|
||||||
|
timeout=30 # Add timeout of 30 seconds
|
||||||
)
|
)
|
||||||
|
|
||||||
if response.status_code == 401:
|
if response.status_code == 401:
|
||||||
return {"error": f"Authentication failed (401). Check your API key."}
|
return {"error": "Authentication failed (401). Check your API key."}
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
return {"error": f"API request failed with status code {response.status_code}"}
|
return {"error": f"API request failed with status code {response.status_code}"}
|
||||||
@ -1491,7 +1496,9 @@ class BlenderMCPServer:
|
|||||||
return {"error": f"Unexpected response format from Sketchfab API: {response_data}"}
|
return {"error": f"Unexpected response format from Sketchfab API: {response_data}"}
|
||||||
|
|
||||||
return response_data
|
return response_data
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
return {"error": "Request timed out. Check your internet connection."}
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
return {"error": f"Invalid JSON response from Sketchfab API: {str(e)}"}
|
return {"error": f"Invalid JSON response from Sketchfab API: {str(e)}"}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -1511,17 +1518,17 @@ class BlenderMCPServer:
|
|||||||
"Authorization": f"Token {api_key}"
|
"Authorization": f"Token {api_key}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Request download URL using the exact endpoint from the documentation
|
# Request download URL using the exact endpoint from the documentation
|
||||||
download_endpoint = f"https://api.sketchfab.com/v3/models/{uid}/download"
|
download_endpoint = f"https://api.sketchfab.com/v3/models/{uid}/download"
|
||||||
|
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
download_endpoint,
|
download_endpoint,
|
||||||
headers=headers
|
headers=headers,
|
||||||
|
timeout=30 # Add timeout of 30 seconds
|
||||||
)
|
)
|
||||||
|
|
||||||
if response.status_code == 401:
|
if response.status_code == 401:
|
||||||
return {"error": f"Authentication failed (401). Check your API key."}
|
return {"error": "Authentication failed (401). Check your API key."}
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
return {"error": f"Download request failed with status code {response.status_code}"}
|
return {"error": f"Download request failed with status code {response.status_code}"}
|
||||||
@ -1532,7 +1539,6 @@ class BlenderMCPServer:
|
|||||||
if data is None:
|
if data is None:
|
||||||
return {"error": "Received empty response from Sketchfab API for download request"}
|
return {"error": "Received empty response from Sketchfab API for download request"}
|
||||||
|
|
||||||
|
|
||||||
# Extract download URL with safety checks
|
# Extract download URL with safety checks
|
||||||
gltf_data = data.get("gltf")
|
gltf_data = data.get("gltf")
|
||||||
if not gltf_data:
|
if not gltf_data:
|
||||||
@ -1542,29 +1548,55 @@ class BlenderMCPServer:
|
|||||||
if not download_url:
|
if not download_url:
|
||||||
return {"error": "No download URL available for this model. Make sure the model is downloadable and you have access."}
|
return {"error": "No download URL available for this model. Make sure the model is downloadable and you have access."}
|
||||||
|
|
||||||
|
# Download the model (already has timeout)
|
||||||
# Download the model
|
model_response = requests.get(download_url, timeout=60) # 60 second timeout
|
||||||
model_response = requests.get(download_url)
|
|
||||||
|
|
||||||
if model_response.status_code != 200:
|
if model_response.status_code != 200:
|
||||||
return {"error": f"Model download failed with status code {model_response.status_code}"}
|
return {"error": f"Model download failed with status code {model_response.status_code}"}
|
||||||
|
|
||||||
# Save to temporary file
|
# Save to temporary file
|
||||||
temp_dir = tempfile.mkdtemp()
|
temp_dir = tempfile.mkdtemp()
|
||||||
file_path = os.path.join(temp_dir, f"{uid}.zip")
|
zip_file_path = os.path.join(temp_dir, f"{uid}.zip")
|
||||||
|
|
||||||
|
with open(zip_file_path, "wb") as f:
|
||||||
with open(file_path, "wb") as f:
|
|
||||||
f.write(model_response.content)
|
f.write(model_response.content)
|
||||||
|
|
||||||
# Extract the zip file
|
# Extract the zip file with enhanced security
|
||||||
with zipfile.ZipFile(file_path, 'r') as zip_ref:
|
with zipfile.ZipFile(zip_file_path, 'r') as zip_ref:
|
||||||
|
# More secure zip slip prevention
|
||||||
|
for file_info in zip_ref.infolist():
|
||||||
|
# Get the path of the file
|
||||||
|
file_path = file_info.filename
|
||||||
|
|
||||||
|
# Convert directory separators to the current OS style
|
||||||
|
# This handles both / and \ in zip entries
|
||||||
|
target_path = os.path.join(temp_dir, os.path.normpath(file_path))
|
||||||
|
|
||||||
|
# Get absolute paths for comparison
|
||||||
|
abs_temp_dir = os.path.abspath(temp_dir)
|
||||||
|
abs_target_path = os.path.abspath(target_path)
|
||||||
|
|
||||||
|
# Ensure the normalized path doesn't escape the target directory
|
||||||
|
if not abs_target_path.startswith(abs_temp_dir):
|
||||||
|
with suppress(Exception):
|
||||||
|
shutil.rmtree(temp_dir)
|
||||||
|
return {"error": "Security issue: Zip contains files with path traversal attempt"}
|
||||||
|
|
||||||
|
# Additional explicit check for directory traversal
|
||||||
|
if ".." in file_path:
|
||||||
|
with suppress(Exception):
|
||||||
|
shutil.rmtree(temp_dir)
|
||||||
|
return {"error": "Security issue: Zip contains files with directory traversal sequence"}
|
||||||
|
|
||||||
|
# If all files passed security checks, extract them
|
||||||
zip_ref.extractall(temp_dir)
|
zip_ref.extractall(temp_dir)
|
||||||
|
|
||||||
# Find the main glTF file
|
# Find the main glTF file
|
||||||
gltf_files = [f for f in os.listdir(temp_dir) if f.endswith('.gltf') or f.endswith('.glb')]
|
gltf_files = [f for f in os.listdir(temp_dir) if f.endswith('.gltf') or f.endswith('.glb')]
|
||||||
|
|
||||||
if not gltf_files:
|
if not gltf_files:
|
||||||
|
with suppress(Exception):
|
||||||
|
shutil.rmtree(temp_dir)
|
||||||
return {"error": "No glTF file found in the downloaded model"}
|
return {"error": "No glTF file found in the downloaded model"}
|
||||||
|
|
||||||
main_file = os.path.join(temp_dir, gltf_files[0])
|
main_file = os.path.join(temp_dir, gltf_files[0])
|
||||||
@ -1576,17 +1608,17 @@ class BlenderMCPServer:
|
|||||||
imported_objects = [obj.name for obj in bpy.context.selected_objects]
|
imported_objects = [obj.name for obj in bpy.context.selected_objects]
|
||||||
|
|
||||||
# Clean up temporary files
|
# Clean up temporary files
|
||||||
try:
|
with suppress(Exception):
|
||||||
shutil.rmtree(temp_dir)
|
shutil.rmtree(temp_dir)
|
||||||
except Exception as e:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": f"Model imported successfully",
|
"message": "Model imported successfully",
|
||||||
"imported_objects": imported_objects
|
"imported_objects": imported_objects
|
||||||
}
|
}
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
return {"error": "Request timed out. Check your internet connection and try again with a simpler model."}
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
return {"error": f"Invalid JSON response from Sketchfab API: {str(e)}"}
|
return {"error": f"Invalid JSON response from Sketchfab API: {str(e)}"}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@ -844,6 +844,7 @@ def asset_creation_strategy() -> str:
|
|||||||
- For materials/textures: Use download_polyhaven_asset() with asset_type="textures"
|
- For materials/textures: Use download_polyhaven_asset() with asset_type="textures"
|
||||||
- For environment lighting: Use download_polyhaven_asset() with asset_type="hdris"
|
- For environment lighting: Use download_polyhaven_asset() with asset_type="hdris"
|
||||||
2. Sketchfab
|
2. Sketchfab
|
||||||
|
Sketchfab is good at Realistic models, and has a wider variety of models than PolyHaven.
|
||||||
Use get_sketchfab_status() to verify its status
|
Use get_sketchfab_status() to verify its status
|
||||||
If Sketchfab is enabled:
|
If Sketchfab is enabled:
|
||||||
- For objects/models: First search using search_sketchfab_models() with your query
|
- For objects/models: First search using search_sketchfab_models() with your query
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user