Restrict text_image font paths to font and input folders

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mcDandy
2026-10-18 12:54:46 +02:00
co-authored by Claude Opus 5.5
parent 08fbf104ea
commit 977ce75c89
5 changed files with 31 additions and 14 deletions
+1 -1
View File
@@ -1383,7 +1383,7 @@ func3:
/**
text_image(text, font, size, [max_width], [weight], [rotation_angle], [line_spacing], [italic], [underline]) - renders text to a 2D tensor with values 0-1
- text: text to render; line breaks start new lines
- font: font name to look up in the system fonts, or a path to a font file
- font: font name to look up in the system fonts, or a path to a font file in a system font folder or the ComfyUI input folder
- size: font size in pixels
- max_width: wrap lines wider than this many pixels (default: 0, no wrapping)
- weight: font weight 100-900 as in CSS (default: 400)
+18 -5
View File
@@ -10,6 +10,7 @@ import math
import inspect
import weakref
import torch.nn.functional as F
import folder_paths
from comfy.nested_tensor import NestedTensor
from comfy.model_management import throw_exception_if_processing_interrupted
from comfy.k_diffusion import sampling as k_sampling
@@ -190,9 +191,7 @@ def _font_axis_name(axis):
return str(name).lower()
@functools.lru_cache(maxsize=1)
def _system_fonts():
"""Index of installed fonts, scanned once per process (fonts installed later need a restart)."""
def _font_dirs():
if sys.platform == "win32":
search_dirs = [os.path.join(os.environ.get("WINDIR", "C:\\Windows"), "Fonts"), os.path.expandvars(r"%LOCALAPPDATA%\Microsoft\Windows\Fonts")]
elif sys.platform == "darwin":
@@ -205,9 +204,23 @@ def _system_fonts():
os.path.expanduser("~/.local/share/fonts"),
os.path.expanduser("~/.fonts"),
]
return list(dict.fromkeys(search_dirs))
def _is_allowed_font_file(path):
"""Font files given by path must be in a system font folder or in ComfyUI's input folder."""
if not os.path.isfile(path):
return False
path = os.path.normcase(os.path.realpath(path))
roots = _font_dirs() + [folder_paths.get_input_directory()]
return any(path.startswith(os.path.normcase(os.path.realpath(root)) + os.sep) for root in roots)
@functools.lru_cache(maxsize=1)
def _system_fonts():
"""Index of installed fonts, scanned once per process (fonts installed later need a restart)."""
fonts = []
for directory in dict.fromkeys(search_dirs):
for directory in _font_dirs():
for root, _, files in os.walk(directory):
for fname in files:
if not fname.lower().endswith((".ttf", ".otf", ".ttc", ".otc")):
@@ -3536,7 +3549,7 @@ class UnifiedMathVisitor(MathExprVisitor):
tier = _FONT_WEIGHT_TIERS[bisect.bisect_left((150, 250, 350, 450, 550, 650, 750, 850), weight_val)]
synthetic_italic = synthetic_bold = False
variation_weight, variation_italic = weight_val, is_italic
if os.path.isfile(font_name):
if _is_allowed_font_file(font_name):
font_path = font_name
else:
font_path = _find_font(font_name, tier, weight_val, is_italic)
+1 -1
View File
@@ -264,7 +264,7 @@ INBUILT_FUNCTION_META = {
'interpolate_area': {'min_args': 2, 'max_args': 2, 'signature': 'interpolate_area(x, scale)', 'forms': [['x', 'scale']], 'description': 'resizes x [N, C, ...] with area interpolation. A number scales every spatial dimension; a list is the target spatial size', 'details': '', 'params': {'x': 'tensor shaped [N, C, ...] with 1-3 spatial dimensions', 'scale': 'a number scales every spatial dimension; a list gives the target spatial size'}, 'aliases': []},
'interpolate_nearest': {'min_args': 2, 'max_args': 2, 'signature': 'interpolate_nearest(x, scale)', 'forms': [['x', 'scale']], 'description': 'resizes x [N, C, ...] with nearest-neighbour interpolation. A number scales every spatial dimension; a list is the target spatial size', 'details': '', 'params': {'x': 'tensor shaped [N, C, ...] with 1-3 spatial dimensions', 'scale': 'a number scales every spatial dimension; a list gives the target spatial size'}, 'aliases': ['interpolate_nearest_exact']},
'interpolate_nearest_exact': {'min_args': 2, 'max_args': 2, 'signature': 'interpolate_nearest_exact(x, scale)', 'forms': [['x', 'scale']], 'description': 'resizes x [N, C, ...] with nearest-neighbour interpolation. A number scales every spatial dimension; a list is the target spatial size', 'details': '', 'params': {'x': 'tensor shaped [N, C, ...] with 1-3 spatial dimensions', 'scale': 'a number scales every spatial dimension; a list gives the target spatial size'}, 'aliases': ['interpolate_nearest']},
'text_image': {'min_args': 3, 'max_args': 9, 'signature': 'text_image(text, font, size, [max_width], [weight], [rotation_angle], [line_spacing], [italic], [underline])', 'forms': [['text', 'font', 'size', '[max_width]', '[weight]', '[rotation_angle]', '[line_spacing]', '[italic]', '[underline]']], 'description': 'renders text to a 2D tensor with values 0-1', 'details': '', 'params': {'text': 'text to render; line breaks start new lines', 'font': 'font name to look up in the system fonts, or a path to a font file', 'size': 'font size in pixels', 'max_width': 'wrap lines wider than this many pixels (default: 0, no wrapping)', 'weight': 'font weight 100-900 as in CSS (default: 400)', 'rotation_angle': 'rotation in degrees, counter-clockwise (default: 0)', 'line_spacing': 'line distance as a multiple of the line height (default: 1)', 'italic': 'truthy for italic text (default: false)', 'underline': 'truthy to underline the text (default: false)'}, 'aliases': []},
'text_image': {'min_args': 3, 'max_args': 9, 'signature': 'text_image(text, font, size, [max_width], [weight], [rotation_angle], [line_spacing], [italic], [underline])', 'forms': [['text', 'font', 'size', '[max_width]', '[weight]', '[rotation_angle]', '[line_spacing]', '[italic]', '[underline]']], 'description': 'renders text to a 2D tensor with values 0-1', 'details': '', 'params': {'text': 'text to render; line breaks start new lines', 'font': 'font name to look up in the system fonts, or a path to a font file in a system font folder or the ComfyUI input folder', 'size': 'font size in pixels', 'max_width': 'wrap lines wider than this many pixels (default: 0, no wrapping)', 'weight': 'font weight 100-900 as in CSS (default: 400)', 'rotation_angle': 'rotation in degrees, counter-clockwise (default: 0)', 'line_spacing': 'line distance as a multiple of the line height (default: 1)', 'italic': 'truthy for italic text (default: false)', 'underline': 'truthy to underline the text (default: false)'}, 'aliases': []},
'as_nested_tensor': {'min_args': 1, 'max_args': 1, 'signature': 'as_nested_tensor(values)', 'forms': [['values']], 'description': 'converts a list to a nested tensor (special object containing tensors of different shapes behaving like a tensor)', 'details': '', 'params': {'values': 'list of tensors; their shapes may differ'}, 'aliases': ['as_nested']},
'as_nested': {'min_args': 1, 'max_args': 1, 'signature': 'as_nested(values)', 'forms': [['values']], 'description': 'converts a list to a nested tensor (special object containing tensors of different shapes behaving like a tensor)', 'details': '', 'params': {'values': 'list of tensors; their shapes may differ'}, 'aliases': ['as_nested_tensor']},
'svd': {'min_args': 1, 'max_args': 2, 'signature': 'svd(x, [full_matrices])', 'forms': [['x', '[full_matrices]']], 'description': 'computes the singular value decomposition of x. Returns a list of 3 tensors U, S, Vh such that x = matmul(matmul(U, diagonal_matrix(S, shape(U))), Vh)', 'details': '', 'params': {'x': 'matrix, or a batch of matrices in the last two dimensions', 'full_matrices': 'truthy to return full-size U and Vh (default: false)'}, 'aliases': ['singular_value_decomposition']},
+10 -6
View File
@@ -80,17 +80,12 @@ def test_lambda_calls_require_exact_argument_count(call):
def test_text_image_size_expression_is_evaluated_once(monkeypatch):
visitor_module = importlib.import_module("more_math.Parser.UnifiedMathVisitor")
default_font = ImageFont.load_default()
original_isfile = os.path.isfile
monkeypatch.setattr(
visitor_module.ImageFont,
"truetype",
lambda *args, **kwargs: default_font,
)
monkeypatch.setattr(
visitor_module.os.path,
"isfile",
lambda path: path == "mock-font" or original_isfile(path),
)
monkeypatch.setattr(visitor_module, "_is_allowed_font_file", lambda path: path == "mock-font")
values = MathDict()
_visit(
@@ -101,6 +96,15 @@ def test_text_image_size_expression_is_evaluated_once(monkeypatch):
assert len(values) == 1
def test_text_image_rejects_font_path_outside_font_folders(monkeypatch):
visitor_module = importlib.import_module("more_math.Parser.UnifiedMathVisitor")
monkeypatch.setattr(visitor_module, "_find_font", lambda *args: None)
path = os.path.abspath(__file__).replace("\\", "/")
with pytest.raises(FileNotFoundError):
_visit(f'text_image("x", "{path}", 12)', {})
def test_stack_list_of_tensors_with_negative_dimension():
first = torch.tensor([1.0, 2.0])
second = torch.tensor([3.0, 4.0])
+1 -1
View File
@@ -267,7 +267,7 @@ export const FUNCTION_META = {
"interpolate_area": { minArgs: 2, maxArgs: 2, signature: "interpolate_area(x, scale)", forms: [["x", "scale"]], description: "resizes x [N, C, ...] with area interpolation. A number scales every spatial dimension; a list is the target spatial size", details: "", params: {"x": "tensor shaped [N, C, ...] with 1-3 spatial dimensions", "scale": "a number scales every spatial dimension; a list gives the target spatial size"}, aliases: [] },
"interpolate_nearest": { minArgs: 2, maxArgs: 2, signature: "interpolate_nearest(x, scale)", forms: [["x", "scale"]], description: "resizes x [N, C, ...] with nearest-neighbour interpolation. A number scales every spatial dimension; a list is the target spatial size", details: "", params: {"x": "tensor shaped [N, C, ...] with 1-3 spatial dimensions", "scale": "a number scales every spatial dimension; a list gives the target spatial size"}, aliases: ["interpolate_nearest_exact"] },
"interpolate_nearest_exact": { minArgs: 2, maxArgs: 2, signature: "interpolate_nearest_exact(x, scale)", forms: [["x", "scale"]], description: "resizes x [N, C, ...] with nearest-neighbour interpolation. A number scales every spatial dimension; a list is the target spatial size", details: "", params: {"x": "tensor shaped [N, C, ...] with 1-3 spatial dimensions", "scale": "a number scales every spatial dimension; a list gives the target spatial size"}, aliases: ["interpolate_nearest"] },
"text_image": { minArgs: 3, maxArgs: 9, signature: "text_image(text, font, size, [max_width], [weight], [rotation_angle], [line_spacing], [italic], [underline])", forms: [["text", "font", "size", "[max_width]", "[weight]", "[rotation_angle]", "[line_spacing]", "[italic]", "[underline]"]], description: "renders text to a 2D tensor with values 0-1", details: "", params: {"text": "text to render; line breaks start new lines", "font": "font name to look up in the system fonts, or a path to a font file", "size": "font size in pixels", "max_width": "wrap lines wider than this many pixels (default: 0, no wrapping)", "weight": "font weight 100-900 as in CSS (default: 400)", "rotation_angle": "rotation in degrees, counter-clockwise (default: 0)", "line_spacing": "line distance as a multiple of the line height (default: 1)", "italic": "truthy for italic text (default: false)", "underline": "truthy to underline the text (default: false)"}, aliases: [] },
"text_image": { minArgs: 3, maxArgs: 9, signature: "text_image(text, font, size, [max_width], [weight], [rotation_angle], [line_spacing], [italic], [underline])", forms: [["text", "font", "size", "[max_width]", "[weight]", "[rotation_angle]", "[line_spacing]", "[italic]", "[underline]"]], description: "renders text to a 2D tensor with values 0-1", details: "", params: {"text": "text to render; line breaks start new lines", "font": "font name to look up in the system fonts, or a path to a font file in a system font folder or the ComfyUI input folder", "size": "font size in pixels", "max_width": "wrap lines wider than this many pixels (default: 0, no wrapping)", "weight": "font weight 100-900 as in CSS (default: 400)", "rotation_angle": "rotation in degrees, counter-clockwise (default: 0)", "line_spacing": "line distance as a multiple of the line height (default: 1)", "italic": "truthy for italic text (default: false)", "underline": "truthy to underline the text (default: false)"}, aliases: [] },
"as_nested_tensor": { minArgs: 1, maxArgs: 1, signature: "as_nested_tensor(values)", forms: [["values"]], description: "converts a list to a nested tensor (special object containing tensors of different shapes behaving like a tensor)", details: "", params: {"values": "list of tensors; their shapes may differ"}, aliases: ["as_nested"] },
"as_nested": { minArgs: 1, maxArgs: 1, signature: "as_nested(values)", forms: [["values"]], description: "converts a list to a nested tensor (special object containing tensors of different shapes behaving like a tensor)", details: "", params: {"values": "list of tensors; their shapes may differ"}, aliases: ["as_nested_tensor"] },
"svd": { minArgs: 1, maxArgs: 2, signature: "svd(x, [full_matrices])", forms: [["x", "[full_matrices]"]], description: "computes the singular value decomposition of x. Returns a list of 3 tensors U, S, Vh such that x = matmul(matmul(U, diagonal_matrix(S, shape(U))), Vh)", details: "", params: {"x": "matrix, or a batch of matrices in the last two dimensions", "full_matrices": "truthy to return full-size U and Vh (default: false)"}, aliases: ["singular_value_decomposition"] },