diff --git a/pyp2rpm/filters.py b/pyp2rpm/filters.py index f6e59e1..3f9b0a3 100644 --- a/pyp2rpm/filters.py +++ b/pyp2rpm/filters.py @@ -126,6 +126,28 @@ def rpm_version(version, use_macro=True): return '{}~{}'.format(rpm_version, rpm_suffix) +def rpm_escape(text): + """Escapes RPM directives and macros in text to prevent code injection. + + RPM spec files interpret percent signs (%) as the start of macros, + directives, or Lua scriptlets. To prevent malicious package metadata + from injecting arbitrary commands, all percent signs must be escaped + by doubling them (%%). + + Args: + text: String that may contain user-controlled content + + Returns: + String with all percent signs escaped (% becomes %%) + """ + if text is None: + return '' + if not isinstance(text, str): + text = str(text) + # Escape all percent signs by doubling them + return text.replace('%', '%%') + + __all__ = [name_for_python_version, script_name_for_python_version, sitedir_for_python_version, @@ -135,4 +157,5 @@ def rpm_version(version, use_macro=True): package_to_path, macroed_url, rpm_version_410, - rpm_version] + rpm_version, + rpm_escape] diff --git a/pyp2rpm/package_data.py b/pyp2rpm/package_data.py index d875d9d..8609667 100644 --- a/pyp2rpm/package_data.py +++ b/pyp2rpm/package_data.py @@ -5,6 +5,7 @@ from pyp2rpm import version from pyp2rpm import utils +from pyp2rpm import filters logger = logging.getLogger(__name__) @@ -55,8 +56,16 @@ def __getattr__(self, name): return self.data.get(name, 'TODO:') def __setattr__(self, name, value): + # Fields that need RPM directive escaping (user-controlled metadata) + FIELDS_TO_ESCAPE = ['summary', 'description', 'license', 'home_page', 'name'] + if name == 'summary' and isinstance(value, utils.str_classes): value = value.rstrip('.').replace('\n', ' ') + + # Escape RPM directives in user-controlled fields to prevent code injection + if name in FIELDS_TO_ESCAPE and isinstance(value, utils.str_classes): + value = filters.rpm_escape(value) + if value is not None: self.data[name] = value