From 82265a53e80fd14b8284f8baa231cdf900a5e04a Mon Sep 17 00:00:00 2001 From: jav1er8 <93607250+jav1er8@users.noreply.github.com> Date: Wed, 9 Sep 2026 13:25:21 -0400 Subject: [PATCH] FIX: reject non-finite parameters in the remaining audio and image converters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #2560 fixed AudioSpeedConverter and #2566 fixed AudioEchoConverter by rejecting non-finite values. The same validation pattern is used across the converter family, and four converters still have the gap: `x <= 0 or x > N` never rejects NaN, because every comparison against NaN is false. Four parameters accepted NaN and/or infinity before this change: AudioWhiteNoiseConverter.noise_scale NaN AudioVolumeConverter.volume_factor NaN, inf ImageColorSaturationConverter.level NaN, inf ImageRotationConverter.angle NaN, inf (never validated at all) The audio and image converters then corrupt their output silently, which matters for a red-teaming tool: the artifact is written, stored in memory and sent to the target as if it were valid. AudioWhiteNoiseConverter(noise_scale=nan) rng.normal(0, nan * max_val) makes the whole array NaN. np.clip does not repair NaN, and the int16 cast turns it into zeros — silent audio, no error. AudioVolumeConverter(volume_factor=inf) 0 * inf = nan on silent samples, everything else saturates. ImageColorSaturationConverter(level=nan), ImageRotationConverter(angle=nan) PIL returns an all-black image. No exception. Adds math.isfinite to each validation and validates ImageRotationConverter.angle, which had no check. Error messages now name finiteness, so the existing tests that match on them are updated the same way #2560 and #2566 updated theirs. Tests follow the parametrized pattern introduced in #2560 and cover nan, inf and -inf for every affected parameter. --- pyrit/converter/audio_volume_converter.py | 9 +++++---- pyrit/converter/audio_white_noise_converter.py | 11 ++++++----- pyrit/converter/image_color_saturation_converter.py | 10 ++++++---- pyrit/converter/image_rotation_converter.py | 8 ++++++-- tests/unit/converter/test_audio_volume_converter.py | 11 +++++++++-- .../converter/test_audio_white_noise_converter.py | 13 ++++++++++--- .../test_image_color_saturation_converter.py | 9 ++++++++- .../unit/converter/test_image_rotation_converter.py | 7 +++++++ 8 files changed, 57 insertions(+), 21 deletions(-) diff --git a/pyrit/converter/audio_volume_converter.py b/pyrit/converter/audio_volume_converter.py index 0d0afa76dc..f746c74d43 100644 --- a/pyrit/converter/audio_volume_converter.py +++ b/pyrit/converter/audio_volume_converter.py @@ -3,6 +3,7 @@ import io import logging +import math from typing import Any, Literal import numpy as np @@ -46,13 +47,13 @@ def __init__( output_format (str): The format of the audio file, defaults to "wav". volume_factor (float): The factor by which to scale the volume. Values > 1.0 increase volume, values < 1.0 decrease volume. - Must be greater than 0. Defaults to 1.5. + Must be finite and greater than 0. Defaults to 1.5. Raises: - ValueError: If volume_factor is not positive. + ValueError: If volume_factor is non-finite or not positive. """ - if volume_factor <= 0: - raise ValueError("volume_factor must be greater than 0.") + if not math.isfinite(volume_factor) or volume_factor <= 0: + raise ValueError("volume_factor must be finite and greater than 0.") self._output_format = output_format self._volume_factor = volume_factor diff --git a/pyrit/converter/audio_white_noise_converter.py b/pyrit/converter/audio_white_noise_converter.py index 35675091d0..7a7e219fdd 100644 --- a/pyrit/converter/audio_white_noise_converter.py +++ b/pyrit/converter/audio_white_noise_converter.py @@ -3,6 +3,7 @@ import io import logging +import math from typing import Any, Literal import numpy as np @@ -44,14 +45,14 @@ def __init__( output_format (str): The format of the audio file, defaults to "wav". noise_scale (float): Controls the amplitude of the added noise, expressed as a fraction of the signal's maximum possible value. For int16 audio - the noise amplitude will be noise_scale * 32767. Must be greater than 0 - and at most 1.0. Defaults to 0.02. + the noise amplitude will be noise_scale * 32767. Must be finite, greater + than 0, and at most 1.0. Defaults to 0.02. Raises: - ValueError: If noise_scale is not in (0, 1]. + ValueError: If noise_scale is non-finite or not in (0, 1]. """ - if noise_scale <= 0 or noise_scale > 1.0: - raise ValueError("noise_scale must be between 0 (exclusive) and 1.0 (inclusive).") + if not math.isfinite(noise_scale) or noise_scale <= 0 or noise_scale > 1.0: + raise ValueError("noise_scale must be finite, greater than 0, and at most 1.0.") self._output_format = output_format self._noise_scale = noise_scale diff --git a/pyrit/converter/image_color_saturation_converter.py b/pyrit/converter/image_color_saturation_converter.py index 4baae25b02..239edcc7bc 100644 --- a/pyrit/converter/image_color_saturation_converter.py +++ b/pyrit/converter/image_color_saturation_converter.py @@ -2,6 +2,7 @@ # Licensed under the MIT license. import logging +import math from typing import Literal from PIL import Image, ImageEnhance @@ -41,13 +42,14 @@ def __init__( 0.0 produces a grayscale image (black and white). 1.0 preserves the original colors. Values greater than 1.0 oversaturate the colors. - Defaults to 0.0 (grayscale image). + Must be finite. Defaults to 0.0 (grayscale image). Raises: - ValueError: If unsupported output format is specified, or if level is negative. + ValueError: If unsupported output format is specified, or if level is non-finite + or negative. """ - if level < 0: - raise ValueError(f"Level must be non-negative, got {level}") + if not math.isfinite(level) or level < 0: + raise ValueError(f"Level must be finite and non-negative, got {level}") self._level = level super().__init__(output_format=output_format) diff --git a/pyrit/converter/image_rotation_converter.py b/pyrit/converter/image_rotation_converter.py index 865366ad17..8c7efc0514 100644 --- a/pyrit/converter/image_rotation_converter.py +++ b/pyrit/converter/image_rotation_converter.py @@ -2,6 +2,7 @@ # Licensed under the MIT license. import logging +import math from typing import Literal from PIL import Image @@ -40,13 +41,16 @@ def __init__( Must be one of 'JPEG', 'PNG', or 'WEBP'. If None, keeps original format (if supported). angle (float): The rotation angle in degrees (counter-clockwise). - Defaults to 90.0. + Must be finite. Defaults to 90.0. fill_color (tuple[int, int, int]): The RGB color to fill exposed background areas after rotation. Defaults to (255, 255, 255) (white). Raises: - ValueError: If unsupported output format is specified, or if the fill color is out of range. + ValueError: If unsupported output format is specified, if the angle is non-finite, + or if the fill color is out of range. """ + if not math.isfinite(angle): + raise ValueError(f"Angle must be finite, got {angle}") if ( not isinstance(fill_color, tuple) or len(fill_color) != 3 diff --git a/tests/unit/converter/test_audio_volume_converter.py b/tests/unit/converter/test_audio_volume_converter.py index 3a159b00b4..478acca34a 100644 --- a/tests/unit/converter/test_audio_volume_converter.py +++ b/tests/unit/converter/test_audio_volume_converter.py @@ -145,13 +145,13 @@ async def test_convert_async_file_not_found(): def test_invalid_volume_factor_zero(): """volume_factor of 0 should raise ValueError.""" - with pytest.raises(ValueError, match="volume_factor must be greater than 0"): + with pytest.raises(ValueError, match="volume_factor must be finite and greater than 0"): AudioVolumeConverter(volume_factor=0) def test_invalid_volume_factor_negative(): """Negative volume_factor should raise ValueError.""" - with pytest.raises(ValueError, match="volume_factor must be greater than 0"): + with pytest.raises(ValueError, match="volume_factor must be finite and greater than 0"): AudioVolumeConverter(volume_factor=-1.0) @@ -160,3 +160,10 @@ async def test_unsupported_input_type(sqlite_instance): converter = AudioVolumeConverter(volume_factor=1.5) with pytest.raises(ValueError, match="Input type not supported"): await converter.convert_async(prompt="some_file.wav", input_type="text") + + +@pytest.mark.parametrize("volume_factor", [float("nan"), float("inf"), float("-inf")]) +def test_invalid_volume_factor_non_finite(volume_factor: float) -> None: + """Non-finite volume factors should fail during converter construction.""" + with pytest.raises(ValueError, match="volume_factor must be finite"): + AudioVolumeConverter(volume_factor=volume_factor) diff --git a/tests/unit/converter/test_audio_white_noise_converter.py b/tests/unit/converter/test_audio_white_noise_converter.py index d164405805..5f1f1dccc5 100644 --- a/tests/unit/converter/test_audio_white_noise_converter.py +++ b/tests/unit/converter/test_audio_white_noise_converter.py @@ -115,19 +115,19 @@ async def test_white_noise_file_not_found(): def test_white_noise_invalid_scale_zero(): """noise_scale of 0 should raise ValueError.""" - with pytest.raises(ValueError, match="noise_scale must be between 0"): + with pytest.raises(ValueError, match="noise_scale must be finite"): AudioWhiteNoiseConverter(noise_scale=0) def test_white_noise_invalid_scale_negative(): """Negative noise_scale should raise ValueError.""" - with pytest.raises(ValueError, match="noise_scale must be between 0"): + with pytest.raises(ValueError, match="noise_scale must be finite"): AudioWhiteNoiseConverter(noise_scale=-0.1) def test_white_noise_invalid_scale_above_one(): """noise_scale > 1 should raise ValueError.""" - with pytest.raises(ValueError, match="noise_scale must be between 0"): + with pytest.raises(ValueError, match="noise_scale must be finite"): AudioWhiteNoiseConverter(noise_scale=1.5) @@ -170,3 +170,10 @@ async def test_white_noise_initialized_seed_is_repeatable_and_does_not_disturb_n for output_path in output_paths: if os.path.exists(output_path): os.remove(output_path) + + +@pytest.mark.parametrize("noise_scale", [float("nan"), float("inf"), float("-inf")]) +def test_invalid_noise_scale_non_finite(noise_scale: float) -> None: + """Non-finite noise scales should fail during converter construction.""" + with pytest.raises(ValueError, match="noise_scale must be finite"): + AudioWhiteNoiseConverter(noise_scale=noise_scale) diff --git a/tests/unit/converter/test_image_color_saturation_converter.py b/tests/unit/converter/test_image_color_saturation_converter.py index e63594eed7..5c9876fdc4 100644 --- a/tests/unit/converter/test_image_color_saturation_converter.py +++ b/tests/unit/converter/test_image_color_saturation_converter.py @@ -53,7 +53,7 @@ def test_image_color_saturation_converter_initialization_output_format_validatio def test_image_color_saturation_converter_initialization_level_validation(): """Test validation of level parameter.""" for invalid_level in [-0.1, -1.0, -100.0]: - with pytest.raises(ValueError, match="Level must be non-negative"): + with pytest.raises(ValueError, match="Level must be finite and non-negative"): ImageColorSaturationConverter(level=invalid_level) for valid_level in [0.0, 0.5, 1.0, 2.0, 10.0]: @@ -213,3 +213,10 @@ async def test_image_color_saturation_converter_output_format_fallback(): mock_serializer.read_data_async.return_value = img_bytes await converter.convert_async(prompt="test.tiff", input_type="image_path") assert mock_serializer.file_extension == "jpeg" + + +@pytest.mark.parametrize("level", [float("nan"), float("inf"), float("-inf")]) +def test_invalid_level_non_finite(level: float) -> None: + """Non-finite saturation levels should fail during converter construction.""" + with pytest.raises(ValueError, match="Level must be finite"): + ImageColorSaturationConverter(level=level) diff --git a/tests/unit/converter/test_image_rotation_converter.py b/tests/unit/converter/test_image_rotation_converter.py index 420085ab79..705e921f2f 100644 --- a/tests/unit/converter/test_image_rotation_converter.py +++ b/tests/unit/converter/test_image_rotation_converter.py @@ -263,3 +263,10 @@ def test_image_rotation_converter_custom_fill_color(sample_image_bytes): # Check that a corner pixel (exposed area) has the fill color corner_pixel = rotated_image.getpixel((0, 0)) assert corner_pixel[:3] == fill_color + + +@pytest.mark.parametrize("angle", [float("nan"), float("inf"), float("-inf")]) +def test_invalid_angle_non_finite(angle: float) -> None: + """Non-finite rotation angles should fail during converter construction.""" + with pytest.raises(ValueError, match="Angle must be finite"): + ImageRotationConverter(angle=angle)