• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zopefoundation / AccessControl / 41085348020

30 Oct 2026 03:56PM UTC coverage: 82.04% (+0.8%) from 81.275%
41085348020

push

github

web-flow
Add freeze_security_declarations. (#172)

* Add freeze_security_declarations.

Call it once all code has been loaded, for example when Zope has started.
Afterwards, any change to module security declarations, for example with
allow_module or ModuleSecurityInfo, raises Forbidden.  The same is true for
allow_class, allow_type and other changes to ContainerAssertions.
Normal class security declarations with ClassSecurityInfo still work,
because classes may be imported later, for example when loading objects
from the database.  Calling it a second time does nothing, so both Zope
and Plone can call it on startup.

Dangerous modules like os, subprocess and AccessControl itself that are
fully accessible from restricted code are logged as a warning.  Use
check_dangerous_modules to check other modules, also after the freeze.

Also remove the module security declaration for sets, which only existed
in Python 2.

* Add third-party network libraries to the dangerous modules.

These are aiohttp, httpx, pycurl, requests and urllib3.  Requests from
the server itself can reach internal services, and httpx can use local
Unix sockets.

* Note that DANGEROUS_MODULES is not guaranteed to be complete or up to date.

Review comment by Jens.

Co-authored-by: Jens Vagelpohl <jens@plyp.com>

---------

Co-authored-by: Jens Vagelpohl <jens@plyp.com>

716 of 1220 branches covered (58.69%)

Branch coverage included in aggregate %.

291 of 293 new or added lines in 6 files covered. (99.32%)

5414 of 6252 relevant lines covered (86.6%)

4.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.11
/src/AccessControl/tests/testModuleSecurity.py
1
##############################################################################
2
#
3
# Copyright (c) 2008 Zope Foundation and Contributors.
4
#
5
# This software is subject to the provisions of the Zope Public License,
6
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
7
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
8
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
9
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
10
# FOR A PARTICULAR PURPOSE
11
#
12
##############################################################################
13
import unittest
5 ✔
14

15
from AccessControl.ZopeGuards import import_default_level
5 ✔
16

17

18
class ModuleSecurityTests(unittest.TestCase):
5 ✔
19

20
    def setUp(self):
5 ✔
21
        from AccessControl import ModuleSecurityInfo as MSI
5 ✔
22
        mod = 'AccessControl.tests'
5 ✔
23
        MSI('%s.mixed_module' % mod).declarePublic('pub')  # NOQA: D001
5 ✔
24
        MSI('%s.public_module' % mod).declarePublic('pub')  # NOQA: D001
5 ✔
25
        MSI('%s.public_module.submodule' % mod).declarePublic('pub')  # NOQA
5 ✔
26

27
    def tearDown(self):
5 ✔
28
        import sys
5 ✔
29
        for module in ('AccessControl.tests.public_module',
5 ✔
30
                       'AccessControl.tests.public_module.submodule',
31
                       'AccessControl.tests.mixed_module',
32
                       'AccessControl.tests.mixed_module.submodule',
33
                       'AccessControl.tests.private_module',
34
                       'AccessControl.tests.private_module.submodule',
35
                       ):
36
            if module in sys.modules:
5 ✔
37
                del sys.modules[module]
5 ✔
38

39
    def assertUnauth(self, module, fromlist, level=import_default_level):
5 ✔
40
        from zExceptions import Unauthorized
5 ✔
41

42
        from AccessControl.ZopeGuards import guarded_import
5 ✔
43
        self.assertRaises(Unauthorized, guarded_import, module,
5 ✔
44
                          fromlist=fromlist, level=level)
45

46
    def assertAuth(self, module, fromlist, level=import_default_level):
5 ✔
47
        from AccessControl.ZopeGuards import guarded_import
5 ✔
48
        guarded_import(module, fromlist=fromlist, level=level)
5 ✔
49

50
    def test_unprotected_module(self):
5 ✔
51
        self.assertUnauth('os', ())
5 ✔
52

53
    def testPrivateModule(self):
5 ✔
54
        self.assertUnauth('AccessControl.tests.private_module', ())
5 ✔
55
        self.assertUnauth('AccessControl.tests.private_module', ('priv',))
5 ✔
56
        self.assertUnauth('AccessControl.tests.private_module.submodule', ())
5 ✔
57
        self.assertUnauth('AccessControl.tests.private_module.submodule',
5 ✔
58
                          ('priv',))
59

60
    def testMixedModule(self):
5 ✔
61
        self.assertAuth('AccessControl.tests.mixed_module', ())
5 ✔
62
        self.assertAuth('AccessControl.tests.mixed_module', ('pub',))
5 ✔
63
        self.assertUnauth('AccessControl.tests.mixed_module', ('priv',))
5 ✔
64
        self.assertUnauth('AccessControl.tests.mixed_module.submodule', ())
5 ✔
65

66
    def testPublicModule(self):
5 ✔
67
        self.assertAuth('AccessControl.tests.public_module', ())
5 ✔
68
        self.assertAuth('AccessControl.tests.public_module', ('pub',))
5 ✔
69
        self.assertAuth('AccessControl.tests.public_module.submodule', ())
5 ✔
70
        self.assertAuth('AccessControl.tests.public_module.submodule',
5 ✔
71
                        ('pub',))
72

73
    def testPublicModuleThreaded(self):
5 ✔
74
        """
75
        Import the same module from two threads simultaneously, checking that
76
        this does not result in a race condition.
77
        """
78
        import threading
5 ✔
79
        finished = []
5 ✔
80

81
        def threaded_run():
5 ✔
82
            self.assertAuth('AccessControl.tests.public_module', ())
5 ✔
83
            finished.append(True)
5 ✔
84

85
        threads = [threading.Thread(target=threaded_run) for _ in range(2)]
5 ✔
86

87
        [t.start() for t in threads]
5 ✔
88
        [t.join() for t in threads]
5 ✔
89

90
        self.assertEqual(len(finished), 2)
5 ✔
91

92
    def test_public_module_asterisk_not_allowed(self):
5 ✔
93
        self.assertUnauth('AccessControl.tests.public_module', ('*',))
5 ✔
94

95
    def test_failed_import_keeps_MSI(self):
5 ✔
96
        # LP #281156
97
        from AccessControl import ModuleSecurityInfo as MSI
5 ✔
98
        from AccessControl.SecurityInfo import _moduleSecurity as MS
5 ✔
99
        from AccessControl.ZopeGuards import guarded_import
5 ✔
100
        MSI('AccessControl.tests.nonesuch').declarePublic('pub')  # NOQA: D001
5 ✔
101
        self.assertIn('AccessControl.tests.nonesuch', MS)
5 ✔
102
        self.assertRaises(ImportError,
5 ✔
103
                          guarded_import,
104
                          'AccessControl.tests.nonesuch',
105
                          ())
106
        self.assertIn('AccessControl.tests.nonesuch', MS)
5 ✔
107

108
    def test_level_default(self):
5 ✔
109
        self.assertAuth('AccessControl.tests.public_module', (),
5 ✔
110
                        level=import_default_level)
111

112
    def test_level_nondefault(self):
5 ✔
113
        self.assertUnauth('AccessControl.tests.public_module', (), level=1)
5 ✔
114

115
    def test_allow_class_allow_module_unauthorized(self):
5 ✔
116
        """ Make sure security policy mutators are safe
117

118
        This test doesn't check the module or class guarding functionality per
119
        se but makes sure several powerful security policy mutator functions
120
        cannot be used from untrusted code, like ``allow_module`` and
121
        ``allow_class`` from AccessControl.SecurityInfo and ``allow_type`` from
122
        AccessControl.SimpleObjectPolicies.
123
        """
124
        from AccessControl import ModuleSecurityInfo as MSI
5 ✔
125

126
        # Malicious or careless code may try to remove safeguards
127
        MSI('AccessControl.tests.public_module').declarePublic('allow_class')
5 ✔
128
        MSI('AccessControl.tests.public_module').declarePublic('allow_module')
5 ✔
129
        MSI('AccessControl.tests.public_module').declarePublic('allow_type')
5 ✔
130

131
        # Additional security declarations on the functions prevent this
132
        self.assertUnauth('AccessControl.tests.public_module',
5 ✔
133
                          ('allow_class',))
134
        self.assertUnauth('AccessControl.tests.public_module',
5 ✔
135
                          ('allow_module',))
136
        self.assertUnauth('AccessControl.tests.public_module',
5 ✔
137
                          ('allow_type',))
138

139

140
class FrozenSecurityDeclarationsTests(unittest.TestCase):
5 ✔
141

142
    setUp = ModuleSecurityTests.setUp
5 ✔
143
    assertUnauth = ModuleSecurityTests.assertUnauth
5 ✔
144
    assertAuth = ModuleSecurityTests.assertAuth
5 ✔
145

146
    def tearDown(self):
5 ✔
147
        from AccessControl import frozen
5 ✔
148
        frozen._frozen = False
5 ✔
149
        ModuleSecurityTests.tearDown(self)
5 ✔
150

151
    def assertForbidden(self, func, *args):
5 ✔
152
        from zExceptions import Forbidden
5 ✔
153
        self.assertRaises(Forbidden, func, *args)
5 ✔
154

155
    def assertUnauthAttribute(self, obj, name):
5 ✔
156
        from zExceptions import Unauthorized
5 ✔
157

158
        from AccessControl.ZopeGuards import guarded_getattr
5 ✔
159
        self.assertRaises(Unauthorized, guarded_getattr, obj, name)
5 ✔
160

161
    def test_declared_modules_stay_importable(self):
5 ✔
162
        from AccessControl import freeze_security_declarations
5 ✔
163
        freeze_security_declarations()
5 ✔
164
        self.assertAuth('AccessControl.tests.public_module', ('pub',))
5 ✔
165
        self.assertAuth('AccessControl.tests.public_module.submodule', ())
5 ✔
166
        self.assertAuth('AccessControl.tests.mixed_module', ('pub',))
5 ✔
167
        self.assertUnauth('AccessControl.tests.mixed_module', ('priv',))
5 ✔
168
        self.assertUnauth('AccessControl.tests.private_module', ())
5 ✔
169

170
    def test_new_declarations_forbidden(self):
5 ✔
171
        from AccessControl import ModuleSecurityInfo as MSI
5 ✔
172
        from AccessControl import allow_module
5 ✔
173
        from AccessControl import freeze_security_declarations
5 ✔
174
        freeze_security_declarations()
5 ✔
175
        name = 'AccessControl.tests.private_module'
5 ✔
176
        self.assertForbidden(allow_module, name)
5 ✔
177
        self.assertForbidden(MSI, name)
5 ✔
178
        self.assertUnauth(name, ())
5 ✔
179

180
    def test_existing_declarations_forbidden(self):
5 ✔
181
        from AccessControl import ModuleSecurityInfo as MSI
5 ✔
182
        from AccessControl import freeze_security_declarations
5 ✔
183
        from AccessControl.SecurityInfo import _appliedModuleSecurity
5 ✔
184
        freeze_security_declarations()
5 ✔
185
        self.assertForbidden(MSI, 'AccessControl.tests.mixed_module')
5 ✔
186
        modsec = _appliedModuleSecurity['AccessControl.tests.mixed_module']
5 ✔
187
        self.assertForbidden(modsec.declarePublic, 'priv')
5 ✔
188
        self.assertForbidden(modsec.declarePrivate, 'pub')
5 ✔
189
        self.assertForbidden(modsec.setDefaultAccess, 1)
5 ✔
190
        # The declarations are unchanged.
191
        self.assertAuth('AccessControl.tests.mixed_module', ('pub',))
5 ✔
192
        self.assertUnauth('AccessControl.tests.mixed_module', ('priv',))
5 ✔
193

194
    def test_registry_changes_forbidden(self):
5 ✔
195
        from AccessControl import freeze_security_declarations
5 ✔
196
        from AccessControl.SecurityInfo import _appliedModuleSecurity
5 ✔
197
        from AccessControl.SecurityInfo import _moduleSecurity
5 ✔
198
        freeze_security_declarations()
5 ✔
199
        name = 'AccessControl.tests.public_module'
5 ✔
200
        for registry in (_moduleSecurity, _appliedModuleSecurity):
5 ✔
201
            self.assertForbidden(registry.__setitem__, name, None)
5 ✔
202
            self.assertForbidden(registry.setdefault, name, None)
5 ✔
203
            self.assertForbidden(registry.update, {name: None})
5 ✔
204
            self.assertForbidden(registry.clear)
5 ✔
205
            self.assertForbidden(registry.popitem)
5 ✔
206
        self.assertForbidden(_appliedModuleSecurity.__delitem__, name)
5 ✔
207
        self.assertForbidden(_appliedModuleSecurity.pop, name)
5 ✔
208
        self.assertAuth(name, ('pub',))
5 ✔
209

210
    def test_unimportable_module(self):
5 ✔
211
        from AccessControl import ModuleSecurityInfo as MSI
5 ✔
212
        from AccessControl import freeze_security_declarations
5 ✔
213
        from AccessControl.SecurityInfo import _moduleSecurity as MS
5 ✔
214
        from AccessControl.ZopeGuards import guarded_import
5 ✔
215
        MSI('AccessControl.tests.nonesuch').declarePublic('pub')  # NOQA: D001
5 ✔
216
        freeze_security_declarations()
5 ✔
217
        self.assertIn('AccessControl.tests.nonesuch', MS)
5 ✔
218
        self.assertRaises(ImportError,
5 ✔
219
                          guarded_import,
220
                          'AccessControl.tests.nonesuch',
221
                          ())
222

223
    def test_dangerous_modules_logged(self):
5 ✔
224
        from AccessControl import ModuleSecurityInfo as MSI
5 ✔
225
        from AccessControl import freeze_security_declarations
5 ✔
226
        MSI('AccessControl.tests.nonesuch_all').setDefaultAccess(1)
5 ✔
227
        MSI('AccessControl.tests.nonesuch_dict').setDefaultAccess({'x': 1})
5 ✔
228
        MSI('AccessControl.tests.nonesuch_none')
5 ✔
229
        # Only modules with all names accessible are logged, not those with
230
        # specific public names, like public_module.
231
        dangerous = frozenset([
5 ✔
232
            'AccessControl.tests.public_module',
233
            'AccessControl.tests.nonesuch_all',
234
            'AccessControl.tests.nonesuch_dict',
235
            'AccessControl.tests.nonesuch_none',
236
        ])
237
        with self.assertLogs('SecurityInfo', 'WARNING') as logs:
5 ✔
238
            freeze_security_declarations(dangerous_modules=dangerous)
5 ✔
239
        self.assertEqual(
5 ✔
240
            [record.getMessage() for record in logs.records],
241
            ["Module 'AccessControl.tests.nonesuch_all' is accessible"
242
             " from restricted code."])
243

244
    def test_freeze_twice(self):
5 ✔
245
        from AccessControl import ModuleSecurityInfo as MSI
5 ✔
246
        from AccessControl import freeze_security_declarations
5 ✔
247
        from AccessControl.frozen import is_frozen
5 ✔
248
        MSI('AccessControl.tests.nonesuch_all').setDefaultAccess(1)
5 ✔
249
        dangerous = frozenset(['AccessControl.tests.nonesuch_all'])
5 ✔
250
        with self.assertLogs('SecurityInfo', 'WARNING'):
5 ✔
251
            freeze_security_declarations(dangerous_modules=dangerous)
5 ✔
252
        # The second call does nothing, so it does not log again.
253
        with self.assertNoLogs('SecurityInfo'):
5 ✔
254
            freeze_security_declarations(dangerous_modules=dangerous)
5 ✔
255
        self.assertTrue(is_frozen())
5 ✔
256

257
    def test_check_dangerous_modules(self):
5 ✔
258
        # This can be called separately, also after the freeze.
259
        from AccessControl import ModuleSecurityInfo as MSI
5 ✔
260
        from AccessControl import freeze_security_declarations
5 ✔
261
        from AccessControl.SecurityInfo import check_dangerous_modules
5 ✔
262
        MSI('AccessControl.tests.nonesuch_all').setDefaultAccess(1)
5 ✔
263
        with self.assertNoLogs('SecurityInfo', 'WARNING'):
5 ✔
264
            freeze_security_declarations(dangerous_modules=frozenset())
5 ✔
265
        with self.assertLogs('SecurityInfo', 'WARNING') as logs:
5 ✔
266
            check_dangerous_modules({'AccessControl.tests.nonesuch_all'})
5 ✔
267
        self.assertEqual(
5 ✔
268
            [record.getMessage() for record in logs.records],
269
            ["Module 'AccessControl.tests.nonesuch_all' is accessible"
270
             " from restricted code."])
271

272
    def test_allow_class_forbidden(self):
5 ✔
273
        from AccessControl import allow_class
5 ✔
274
        from AccessControl import freeze_security_declarations
5 ✔
275

276
        class Dummy:
5 ✔
277
            def method(self):
5 ✔
NEW
278
                pass
×
279

280
        freeze_security_declarations()
5 ✔
281
        self.assertForbidden(allow_class, Dummy)
5 ✔
282
        self.assertFalse(hasattr(Dummy, '_security'))
5 ✔
283
        self.assertUnauthAttribute(Dummy(), 'method')
5 ✔
284

285
    def test_allow_type_forbidden(self):
5 ✔
286
        from AccessControl import allow_type
5 ✔
287
        from AccessControl import freeze_security_declarations
5 ✔
288
        from AccessControl.SimpleObjectPolicies import ContainerAssertions
5 ✔
289

290
        class Dummy:
5 ✔
291
            def method(self):
5 ✔
NEW
292
                pass
×
293

294
        freeze_security_declarations()
5 ✔
295
        self.assertForbidden(allow_type, Dummy)
5 ✔
296
        self.assertNotIn(Dummy, ContainerAssertions)
5 ✔
297
        self.assertUnauthAttribute(Dummy(), 'method')
5 ✔
298

299
    def test_container_assertions_changes_forbidden(self):
5 ✔
300
        from AccessControl import freeze_security_declarations
5 ✔
301
        from AccessControl.SimpleObjectPolicies import ContainerAssertions
5 ✔
302
        freeze_security_declarations()
5 ✔
303
        self.assertForbidden(ContainerAssertions.__setitem__, object, 1)
5 ✔
304
        self.assertForbidden(ContainerAssertions.__delitem__, str)
5 ✔
305
        self.assertForbidden(ContainerAssertions.update, {object: 1})
5 ✔
306
        self.assertForbidden(ContainerAssertions.setdefault, object, 1)
5 ✔
307
        self.assertForbidden(ContainerAssertions.pop, str)
5 ✔
308
        self.assertIn(str, ContainerAssertions)
5 ✔
309
        self.assertNotIn(object, ContainerAssertions)
5 ✔
310

311
    def test_class_security_info_still_works(self):
5 ✔
312
        # Classes may be imported after the freeze, for example when
313
        # loading objects from the database.
314
        from AccessControl import ClassSecurityInfo
5 ✔
315
        from AccessControl import freeze_security_declarations
5 ✔
316
        from AccessControl.class_init import InitializeClass
5 ✔
317
        from AccessControl.ZopeGuards import guarded_getattr
5 ✔
318

319
        class Dummy:
5 ✔
320
            security = ClassSecurityInfo()
5 ✔
321

322
            @security.public
5 ✔
323
            def method(self):
5 ✔
324
                return 'ok'
5 ✔
325

326
            @security.private
5 ✔
327
            def private_method(self):
5 ✔
328
                return 'private'
5 ✔
329

330
        freeze_security_declarations()
5 ✔
331
        InitializeClass(Dummy)
5 ✔
332
        self.assertEqual(guarded_getattr(Dummy(), 'method')(), 'ok')
5 ✔
333
        self.assertUnauthAttribute(Dummy(), 'private_method')
5 ✔
334
        # Python code can still call it.
335
        self.assertEqual(Dummy().private_method(), 'private')
5 ✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc