Skip to content

Commit 212e182

Browse files
committed
security: Escape field values that might contain RPM macros
Assisted-by: Claude Signed-off-by: Gordon Messmer <gmessmer@redhat.com>
1 parent 92399da commit 212e182

2 files changed

Lines changed: 33 additions & 1 deletion

File tree

pyp2rpm/filters.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,28 @@ def rpm_version(version, use_macro=True):
126126
return '{}~{}'.format(rpm_version, rpm_suffix)
127127

128128

129+
def rpm_escape(text):
130+
"""Escapes RPM directives and macros in text to prevent code injection.
131+
132+
RPM spec files interpret percent signs (%) as the start of macros,
133+
directives, or Lua scriptlets. To prevent malicious package metadata
134+
from injecting arbitrary commands, all percent signs must be escaped
135+
by doubling them (%%).
136+
137+
Args:
138+
text: String that may contain user-controlled content
139+
140+
Returns:
141+
String with all percent signs escaped (% becomes %%)
142+
"""
143+
if text is None:
144+
return ''
145+
if not isinstance(text, str):
146+
text = str(text)
147+
# Escape all percent signs by doubling them
148+
return text.replace('%', '%%')
149+
150+
129151
__all__ = [name_for_python_version,
130152
script_name_for_python_version,
131153
sitedir_for_python_version,
@@ -135,4 +157,5 @@ def rpm_version(version, use_macro=True):
135157
package_to_path,
136158
macroed_url,
137159
rpm_version_410,
138-
rpm_version]
160+
rpm_version,
161+
rpm_escape]

pyp2rpm/package_data.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from pyp2rpm import version
77
from pyp2rpm import utils
8+
from pyp2rpm import filters
89

910
logger = logging.getLogger(__name__)
1011

@@ -55,8 +56,16 @@ def __getattr__(self, name):
5556
return self.data.get(name, 'TODO:')
5657

5758
def __setattr__(self, name, value):
59+
# Fields that need RPM directive escaping (user-controlled metadata)
60+
FIELDS_TO_ESCAPE = ['summary', 'description', 'license', 'home_page', 'name']
61+
5862
if name == 'summary' and isinstance(value, utils.str_classes):
5963
value = value.rstrip('.').replace('\n', ' ')
64+
65+
# Escape RPM directives in user-controlled fields to prevent code injection
66+
if name in FIELDS_TO_ESCAPE and isinstance(value, utils.str_classes):
67+
value = filters.rpm_escape(value)
68+
6069
if value is not None:
6170
self.data[name] = value
6271

0 commit comments

Comments
 (0)