Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions language_tool_python/config_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Comment thread
mdevolde marked this conversation as resolved.
@dataclass(frozen=True)
class OptionSpec:
"""
Expand Down Expand Up @@ -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-<code>
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-<code>-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)
Expand All @@ -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


Expand Down
35 changes: 35 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import pytest

from language_tool_python.config_file import LanguageToolConfig
from language_tool_python.exceptions import LanguageToolError


Expand Down Expand Up @@ -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)