Skip to content

Commit 8f79376

Browse files
authored
Syncrepl fails to parse SyncInfoMessage when the message is a syncIdSet
Previously, the SyncInfoMessage parser used a for loop to test for the presence of each named choice with with the structure. However, because refreshDelete and refreshPresent both are able to be fully resolved as defaults, and due to how pyasn1 accesses named types, it was not checking the choice tag, and would return a fully populated refreshDelete to the caller instead. This fixes the parser to always return the inner component, and retrieves the name based on the current choice of the tagged item. Author: William Brown <william@blackhats.net.au> #351
1 parent c00694e commit 8f79376

File tree

2 files changed

+68
-25
lines changed

2 files changed

+68
-25
lines changed

Lib/ldap/syncrepl.py

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -314,34 +314,35 @@ def __init__(self, encodedMessage):
314314
self.refreshPresent = None
315315
self.syncIdSet = None
316316

317-
for attr in ['newcookie', 'refreshDelete', 'refreshPresent', 'syncIdSet']:
318-
comp = d[0].getComponentByName(attr)
319-
320-
if comp is not None and comp.hasValue():
321-
322-
if attr == 'newcookie':
323-
self.newcookie = str(comp)
324-
return
317+
# Due to the way pyasn1 works, refreshDelete and refreshPresent are both
318+
# valid in the components as they are fully populated defaults. We must
319+
# get the component directly from the message, not by iteration.
320+
attr = d[0].getName()
321+
comp = d[0].getComponent()
322+
323+
if comp is not None and comp.hasValue():
324+
if attr == 'newcookie':
325+
self.newcookie = str(comp)
326+
return
325327

326-
val = {}
328+
val = {}
327329

328-
cookie = comp.getComponentByName('cookie')
329-
if cookie.hasValue():
330-
val['cookie'] = str(cookie)
330+
cookie = comp.getComponentByName('cookie')
331+
if cookie.hasValue():
332+
val['cookie'] = str(cookie)
331333

332-
if attr.startswith('refresh'):
333-
val['refreshDone'] = bool(comp.getComponentByName('refreshDone'))
334-
elif attr == 'syncIdSet':
335-
uuids = []
336-
ids = comp.getComponentByName('syncUUIDs')
337-
for i in range(len(ids)):
338-
uuid = UUID(bytes=bytes(ids.getComponentByPosition(i)))
339-
uuids.append(str(uuid))
340-
val['syncUUIDs'] = uuids
341-
val['refreshDeletes'] = bool(comp.getComponentByName('refreshDeletes'))
334+
if attr.startswith('refresh'):
335+
val['refreshDone'] = bool(comp.getComponentByName('refreshDone'))
336+
elif attr == 'syncIdSet':
337+
uuids = []
338+
ids = comp.getComponentByName('syncUUIDs')
339+
for i in range(len(ids)):
340+
uuid = UUID(bytes=bytes(ids.getComponentByPosition(i)))
341+
uuids.append(str(uuid))
342+
val['syncUUIDs'] = uuids
343+
val['refreshDeletes'] = bool(comp.getComponentByName('refreshDeletes'))
342344

343-
setattr(self, attr, val)
344-
return
345+
setattr(self, attr, val)
345346

346347

347348
class SyncreplConsumer:

Tests/t_ldap_syncrepl.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import shelve
1111
import sys
1212
import unittest
13+
import binascii
1314

1415
if sys.version_info[0] <= 2:
1516
PY2 = True
@@ -21,7 +22,7 @@
2122

2223
import ldap
2324
from ldap.ldapobject import SimpleLDAPObject
24-
from ldap.syncrepl import SyncreplConsumer
25+
from ldap.syncrepl import SyncreplConsumer, SyncInfoMessage
2526

2627
from slapdtest import SlapdObject, SlapdTestCase
2728

@@ -445,6 +446,47 @@ def setUp(self):
445446
)
446447
self.suffix = self.server.suffix.encode('utf-8')
447448

449+
class DecodeSyncreplProtoTests(unittest.TestCase):
450+
"""
451+
Tests of the ASN.1 decoder for tricky cases or past issues to ensure that
452+
syncrepl messages are handled correctly.
453+
"""
454+
455+
def test_syncidset_message(self):
456+
"""
457+
A syncrepl server may send a sync info message, with a syncIdSet
458+
of uuids to delete. A regression was found in the original
459+
sync info message implementation due to how the choice was
460+
evaluated, because refreshPresent and refreshDelete were both
461+
able to be fully expressed as defaults, causing the parser
462+
to mistakenly catch a syncIdSet as a refreshPresent/refereshDelete.
463+
464+
This tests that a syncIdSet request is properly decoded.
465+
466+
reference: https://tools.ietf.org/html/rfc4533#section-2.5
467+
"""
468+
469+
# This is a dump of a syncidset message from wireshark + 389-ds
470+
msg = """
471+
a36b04526c6461706b64632e6578616d706c652e636f6d3a333839303123636e
472+
3d6469726563746f7279206d616e616765723a64633d6578616d706c652c6463
473+
3d636f6d3a286f626a656374436c6173733d2a2923330101ff311204108dc446
474+
01a93611ea8aaff248c5fa5780
475+
""".replace(' ', '').replace('\n', '')
476+
477+
msgraw = binascii.unhexlify(msg)
478+
sim = SyncInfoMessage(msgraw)
479+
self.assertEqual(sim.refreshDelete, None)
480+
self.assertEqual(sim.refreshPresent, None)
481+
self.assertEqual(sim.newcookie, None)
482+
self.assertEqual(sim.syncIdSet,
483+
{
484+
'cookie': 'ldapkdc.example.com:38901#cn=directory manager:dc=example,dc=com:(objectClass=*)#3',
485+
'syncUUIDs': ['8dc44601-a936-11ea-8aaf-f248c5fa5780'],
486+
'refreshDeletes': True
487+
}
488+
)
489+
448490

449491
if __name__ == '__main__':
450492
unittest.main()

0 commit comments

Comments
 (0)