Skip to content

Commit f930656

Browse files
committed
fix: Escape user-controlled values in constructed XML commands
UserId.get_user_tags, UserId.get_groups, UserId.get_group_members, and VsysOperations.create_import built XML payloads via str.format / string concatenation. Special characters in caller-supplied names (& < > ') either crashed ET.fromstring on the receiving end or altered the structure of the command sent to the device. Wrap the interpolated values with xml.sax.saxutils.escape (element text) and quoteattr (attribute values), and add regression tests.
1 parent 00d6e23 commit f930656

4 files changed

Lines changed: 98 additions & 6 deletions

File tree

panos/base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import time
3030
import xml.dom.minidom as minidom
3131
import xml.etree.ElementTree as ET
32+
from xml.sax.saxutils import escape as xml_escape
3233

3334
import pan.commit
3435
import pan.xapi
@@ -3386,7 +3387,7 @@ def create_import(self, vsys=None):
33863387

33873388
if vsys != "shared" and vsys is not None and self.XPATH_IMPORT is not None:
33883389
xpath = self.xpath_import_base(vsys)
3389-
element = "<member>{0}</member>".format(self.uid)
3390+
element = "<member>{0}</member>".format(xml_escape(self.uid))
33903391
device = self.nearest_pandevice()
33913392
device.active().xapi.set(xpath, element, retry_on_peer=True)
33923393

panos/userid.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import xml.etree.ElementTree as ET
2121
from copy import deepcopy
22+
from xml.sax.saxutils import escape, quoteattr
2223

2324
from pan.xapi import PanXapiError
2425

@@ -550,7 +551,7 @@ def get_groups(self, style=None):
550551
"<show><user><group><list>",
551552
]
552553
if style is not None:
553-
msg.append("<entry name='{0}'/>".format(style))
554+
msg.append("<entry name={0}/>".format(quoteattr(style)))
554555
msg.append("</list></group></user></show>")
555556
cmd = "".join(msg)
556557
vsys = self.device.vsys or "vsys1"
@@ -594,7 +595,11 @@ def get_group_members(self, group):
594595
list
595596
596597
"""
597-
cmd = "<show><user><group><name>" + group + "</name></group></user></show>"
598+
cmd = (
599+
"<show><user><group><name>"
600+
+ escape(group)
601+
+ "</name></group></user></show>"
602+
)
598603
vsys = self.device.vsys or "vsys1"
599604

600605
resp = self.device.op(cmd, vsys=vsys, cmd_xml=False)
@@ -644,12 +649,12 @@ def get_user_tags(self, user=None, prefix=None):
644649
if user is None:
645650
msg.append(
646651
"<all>"
647-
+ "<limit>{0}</limit>".format(limit)
648-
+ "<start-point>{0}</start-point>".format(start)
652+
+ "<limit>{0}</limit>".format(escape(str(limit)))
653+
+ "<start-point>{0}</start-point>".format(escape(str(start)))
649654
+ "</all>"
650655
)
651656
else:
652-
msg.append("<user>{0}</user>".format(user))
657+
msg.append("<user>{0}</user>".format(escape(user)))
653658
msg.append("</registered-user></object></show>")
654659

655660
cmd = ET.fromstring("".join(msg))

tests/test_base.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import pan.xapi
2424
import panos.base as Base
2525
import panos.errors as Err
26+
import panos.firewall
27+
import panos.network
2628

2729

2830
OBJECT_NAME = "MyObjectName"
@@ -1701,5 +1703,30 @@ def test_times_out(self, mocksleep):
17011703
assert mocksleep.call_count == 0
17021704

17031705

1706+
class TestVsysOperationsCreateImport(unittest.TestCase):
1707+
def test_create_import_escapes_uid(self):
1708+
evil_name = "zone&<bad>"
1709+
fw = panos.firewall.Firewall(
1710+
"fw1", "user", "passwd", "authkey", serial="S", vsys="vsys1"
1711+
)
1712+
vlan = panos.network.Vlan(evil_name)
1713+
fw.add(vlan)
1714+
1715+
with mock.patch.object(
1716+
panos.network.Vlan,
1717+
"XPATH_IMPORT",
1718+
new_callable=mock.PropertyMock,
1719+
return_value="/network/vlan",
1720+
):
1721+
fw.xapi.set = mock.Mock()
1722+
vlan.create_import("vsys1")
1723+
1724+
args, _ = fw.xapi.set.call_args
1725+
element = args[1]
1726+
self.assertEqual(element, "<member>zone&amp;&lt;bad&gt;</member>")
1727+
parsed = ET.fromstring(element)
1728+
self.assertEqual(parsed.text, evil_name)
1729+
1730+
17041731
if __name__ == "__main__":
17051732
unittest.main()

tests/test_userid.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import mock
1818
import sys
1919
import unittest
20+
import xml.etree.ElementTree as ET
2021

2122
import panos.firewall
2223
import panos.panorama
@@ -100,5 +101,63 @@ def test_batch_untag_user(self):
100101
)
101102

102103

104+
def test_get_user_tags_escapes_user(self):
105+
evil = "admin</user><all><limit>999999</limit></all><user>x"
106+
fw = panos.firewall.Firewall(
107+
"fw1", "user", "passwd", "authkey", serial="Serial", vsys="vsys1"
108+
)
109+
empty_response = ET.fromstring(
110+
b"<response status='success'><result/></response>"
111+
)
112+
fw.op = mock.Mock(return_value=empty_response)
113+
114+
fw.userid.get_user_tags(user=evil)
115+
116+
sent = fw.op.call_args[1]["cmd"]
117+
parsed = ET.fromstring(sent)
118+
users = parsed.findall("./object/registered-user/user")
119+
self.assertEqual(len(users), 1)
120+
self.assertEqual(users[0].text, evil)
121+
self.assertIsNone(parsed.find("./object/registered-user/all"))
122+
123+
def test_get_groups_escapes_style(self):
124+
evil = "x'/><evil/><entry name='y"
125+
fw = panos.firewall.Firewall(
126+
"fw1", "user", "passwd", "authkey", serial="Serial", vsys="vsys1"
127+
)
128+
empty_response = ET.fromstring(
129+
b"<response status='success'><result>\nTotal: 0\n</result></response>"
130+
)
131+
fw.op = mock.Mock(return_value=empty_response)
132+
133+
fw.userid.get_groups(style=evil)
134+
135+
sent = fw.op.call_args[0][0]
136+
parsed = ET.fromstring(sent)
137+
entries = parsed.findall("./user/group/list/entry")
138+
self.assertEqual(len(entries), 1)
139+
self.assertEqual(entries[0].attrib["name"], evil)
140+
self.assertIsNone(parsed.find(".//evil"))
141+
142+
def test_get_group_members_escapes_group(self):
143+
evil = "g</name><evil/><name>x"
144+
fw = panos.firewall.Firewall(
145+
"fw1", "user", "passwd", "authkey", serial="Serial", vsys="vsys1"
146+
)
147+
empty_response = ET.fromstring(
148+
b"<response status='success'><result>\nTotal: 0\n</result></response>"
149+
)
150+
fw.op = mock.Mock(return_value=empty_response)
151+
152+
fw.userid.get_group_members(evil)
153+
154+
sent = fw.op.call_args[0][0]
155+
parsed = ET.fromstring(sent)
156+
names = parsed.findall("./user/group/name")
157+
self.assertEqual(len(names), 1)
158+
self.assertEqual(names[0].text, evil)
159+
self.assertIsNone(parsed.find(".//evil"))
160+
161+
103162
if __name__ == "__main__":
104163
unittest.main()

0 commit comments

Comments
 (0)