From b70d70894b72593e964eacad135daf6b60d403dd Mon Sep 17 00:00:00 2001 From: mdevolde Date: Thu, 14 May 2026 23:52:30 +0200 Subject: [PATCH] fix(config_file): refuse breaklines in config dict --- language_tool_python/config_file.py | 24 ++++++++++++++++++++ tests/test_config.py | 35 +++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/language_tool_python/config_file.py b/language_tool_python/config_file.py index a7d9836..997353b 100644 --- a/language_tool_python/config_file.py +++ b/language_tool_python/config_file.py @@ -12,6 +12,26 @@ logger = logging.getLogger(__name__) +def _reject_line_breaks(field_name: str, value: str) -> None: + """ + Reject values that would break the one-option-per-line config format. + + :param field_name: The name of the configuration field being validated. + :type field_name: str + :param value: The value of the configuration field to validate. + :type value: str + :raises ValueError: If the value contains line break characters or ends with an odd number of backslashes. + """ + if "\n" in value or "\r" in value: + err = f"config {field_name} cannot contain line breaks" + raise ValueError(err) + + trailing_backslashes = len(value) - len(value.rstrip("\\")) + if trailing_backslashes % 2 == 1: + err = f"config {field_name} cannot end with an odd number of backslashes" + raise ValueError(err) + + @dataclass(frozen=True) class OptionSpec: """ @@ -176,14 +196,17 @@ def _encode_config(config: Dict[str, Any]) -> Dict[str, str]: logger.debug("Encoding LanguageTool config with keys: %s", list(config.keys())) encoded: Dict[str, str] = {} for key, value in config.items(): + _reject_line_breaks("key", key) if _is_lang_key(key) and key.count("-") == 1: # lang- logger.debug("Encoding language option %s=%r", key, value) encoded[key] = str(value) + _reject_line_breaks(key, encoded[key]) continue if _is_lang_key(key) and key.count("-") == 2: # lang--dictPath logger.debug("Encoding language dictPath %s=%r", key, value) _path_validator(value) encoded[key] = _path_encoder(value) + _reject_line_breaks(key, encoded[key]) continue spec = CONFIG_SCHEMA.get(key) @@ -197,6 +220,7 @@ def _encode_config(config: Dict[str, Any]) -> Dict[str, str]: if spec.validator is not None: spec.validator(value) encoded[key] = spec.encoder(value) + _reject_line_breaks(key, encoded[key]) return encoded diff --git a/tests/test_config.py b/tests/test_config.py index 01cb0bb..986affc 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -5,6 +5,7 @@ import pytest +from language_tool_python.config_file import LanguageToolConfig from language_tool_python.exceptions import LanguageToolError @@ -175,3 +176,37 @@ def test_disabled_rule_in_config() -> None: text = "He realised that the organization was in jeopardy." matches = tool.check(text) assert len(matches) == 0 + + +@pytest.mark.parametrize( # type: ignore[untyped-decorator] + "config", + [ + {"blockedReferrers": "example.com\ntrustXForwardForHeader=true"}, + {"disabledRuleIds": ["MORFOLOGIK_RULE_EN_US", "SAFE\rrequestLimit=0"]}, + {"lang-en\ntrustXForwardForHeader": "true"}, + {"lang-en": "custom-word\nrequestLimit=0"}, + ], +) +def test_config_rejects_line_break_injection(config: dict[str, object]) -> None: + """ + Test that config serialization cannot be escaped with CR/LF characters. + """ + with pytest.raises(ValueError, match="cannot contain line breaks"): + LanguageToolConfig(config) + + +@pytest.mark.parametrize( # type: ignore[untyped-decorator] + "config", + [ + {"blockedReferrers": "example.com\\"}, + {"disabledRuleIds": ["MORFOLOGIK_RULE_EN_US", "SAFE\\"]}, + {"lang-en\\": "true"}, + {"lang-en": "custom-word\\"}, + ], +) +def test_config_rejects_odd_trailing_backslashes(config: dict[str, object]) -> None: + """ + Test that config serialization cannot escape the line ending with a backslash. + """ + with pytest.raises(ValueError, match="odd number of backslashes"): + LanguageToolConfig(config)