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

zopefoundation / Zope / 6241206190

19 Sep 2023 09:37PM UTC coverage: 81.148% (+0.03%) from 81.123%
6241206190

Pull #1160

github

mauritsvanrees
Tighten down the ZMI frame source logic to only allow site-local sources.

Do the logic in new method `getZMIMainFrameTarget`.
Do not accept a 'tainted' came_from parameter.
Do not accept a came_from path-only url starting with '//' either.
Browsers can interpret the rest of the path as a domain.
Pull Request #1160: Tighten down ZMI frame source logic to only allow site-local sources [4.x]

4341 of 7076 branches covered (0.0%)

Branch coverage included in aggregate %.

49 of 49 new or added lines in 2 files covered. (100.0%)

27211 of 31806 relevant lines covered (85.55%)

0.86 hits per line

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

79.77
/src/OFS/Application.py
1
##############################################################################
2
#
3
# Copyright (c) 2002 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
"""Application support
1✔
14
"""
15

16
import os
1✔
17
import sys
1✔
18
from logging import getLogger
1✔
19

20
from six.moves.urllib.parse import urlparse
1✔
21

22
import Products
1✔
23
import transaction
1✔
24
from AccessControl import ClassSecurityInfo
1✔
25
from AccessControl.class_init import InitializeClass
1✔
26
from AccessControl.Permission import ApplicationDefaultPermissions
1✔
27
from AccessControl.Permissions import view_management_screens
1✔
28
from AccessControl.tainted import TaintedString
1✔
29
from Acquisition import aq_base
1✔
30
from App import FactoryDispatcher
1✔
31
from App.ApplicationManager import ApplicationManager
1✔
32
from App.ProductContext import ProductContext
1✔
33
from App.version_txt import getZopeVersion
1✔
34
from DateTime import DateTime
1✔
35
from OFS.FindSupport import FindSupport
1✔
36
from OFS.metaconfigure import get_packages_to_initialize
1✔
37
from OFS.metaconfigure import package_initialized
1✔
38
from OFS.userfolder import UserFolder
1✔
39
from webdav.NullResource import NullResource
1✔
40
from zExceptions import Forbidden
1✔
41
from zExceptions import Redirect as RedirectException
1✔
42
from zope.interface import implementer
1✔
43

44
from . import Folder
1✔
45
from . import misc_
1✔
46
from .interfaces import IApplication
1✔
47
from .misc_ import Misc_
1✔
48

49

50
LOG = getLogger('Application')
1✔
51

52
APP_MANAGER = None
1✔
53

54

55
@implementer(IApplication)
1✔
56
class Application(ApplicationDefaultPermissions, Folder.Folder, FindSupport):
1✔
57
    """Top-level system object"""
58

59
    security = ClassSecurityInfo()
1✔
60

61
    title = 'Zope'
1✔
62
    __defined_roles__ = ('Manager', 'Anonymous', 'Owner')
1✔
63
    __error_log__ = None
1✔
64
    isTopLevelPrincipiaApplicationObject = 1
1✔
65

66
    p_ = misc_.p_
1✔
67
    misc_ = misc_.misc_
1✔
68
    _reserved_names = ('Control_Panel', )
1✔
69

70
    # This class-default __allow_groups__ ensures that the
71
    # emergency user can still access the system if the top-level
72
    # UserFolder is deleted. This is necessary to allow people
73
    # to replace the top-level UserFolder object.
74
    __allow_groups__ = UserFolder()
1✔
75

76
    def __init__(self):
1✔
77
        # Initialize users
78
        uf = UserFolder()
1✔
79
        self.__allow_groups__ = uf
1✔
80
        self._setObject('acl_users', uf)
1✔
81

82
    def getId(self):
1✔
83
        try:
1✔
84
            return self.REQUEST['SCRIPT_NAME'][1:]
1✔
85
        except (KeyError, TypeError):
1✔
86
            return self.title
1✔
87

88
    def title_and_id(self):
1✔
89
        return self.title
1✔
90

91
    def title_or_id(self):
1✔
92
        return self.title
1✔
93

94
    def __class_init__(self):
1✔
95
        InitializeClass(self)
1✔
96

97
    @property
1✔
98
    def Control_Panel(self):
1✔
99
        return APP_MANAGER.__of__(self)
1✔
100

101
    def Redirect(self, destination, URL1):
1✔
102
        # Utility function to allow user-controlled redirects.
103
        # No docstring please, we do not want an open redirect
104
        # available as url.
105
        if destination.find('//') >= 0:
1!
106
            raise RedirectException(destination)
1✔
107
        raise RedirectException("%s/%s" % (URL1, destination))
×
108

109
    ZopeRedirect = Redirect
1✔
110

111
    @security.protected(view_management_screens)
1✔
112
    def getZMIMainFrameTarget(self, REQUEST):
1✔
113
        """Utility method to get the right hand side ZMI frame source URL
114

115
        For cases where JavaScript is disabled the ZMI uses a simple REQUEST
116
        variable ``came_from`` to set the source URL for the right hand side
117
        ZMI frame. Since this value can be manipulated by the user it must be
118
        sanity-checked first.
119
        """
120
        parent_url = REQUEST['URL1']
1✔
121
        default = '{}/manage_workspace'.format(parent_url)
1✔
122
        came_from = REQUEST.get('came_from', None)
1✔
123

124
        if not came_from:
1✔
125
            return default
1✔
126

127
        # When came_from contains suspicious code, it will not be a string,
128
        # but an instance of AccessControl.tainted.TaintedString.
129
        # Passing this to urlparse, gives:
130
        # AttributeError: 'str' object has no attribute 'decode'
131
        # This is good, but let's check explicitly.
132
        if isinstance(came_from, TaintedString):
1✔
133
            return default
1✔
134
        try:
1✔
135
            parsed_came_from = urlparse(came_from)
1✔
136
        except AttributeError:
×
137
            return default
×
138
        parsed_parent_url = urlparse(parent_url)
1✔
139

140
        # Only allow a passed-in ``came_from`` URL if it is local (just a path)
141
        # or if the URL scheme and hostname are the same as our own
142
        if (parsed_parent_url.scheme == parsed_came_from.scheme
1✔
143
                and parsed_parent_url.netloc == parsed_came_from.netloc):
144
            return came_from
1✔
145
        if (not parsed_came_from.scheme and not parsed_came_from.netloc):
1✔
146
            # This is only a path.  But some paths can be misinterpreted
147
            # by browsers.
148
            if parsed_came_from.path.startswith("//"):
1✔
149
                return default
1✔
150
            return came_from
1✔
151

152
        return default
1✔
153

154
    def __bobo_traverse__(self, REQUEST, name=None):
1✔
155
        if name is None:
1✔
156
            # Make this more explicit, otherwise getattr(self, name)
157
            # would raise a TypeError getattr(): attribute name must be string
158
            return None
1✔
159

160
        if name == 'Control_Panel':
1!
161
            return APP_MANAGER.__of__(self)
×
162
        try:
1✔
163
            return getattr(self, name)
1✔
164
        except AttributeError:
1✔
165
            pass
1✔
166

167
        try:
1✔
168
            return self[name]
1✔
169
        except KeyError:
1✔
170
            pass
1✔
171

172
        method = REQUEST.get('REQUEST_METHOD', 'GET')
1✔
173
        if NullResource is not None and method not in ('GET', 'POST'):
1✔
174
            return NullResource(self, name, REQUEST).__of__(self)
1✔
175

176
        if method not in ('GET', 'POST'):
1!
177
            return NullResource(self, name, REQUEST).__of__(self)
×
178

179
        # Waaa. unrestrictedTraverse calls us with a fake REQUEST.
180
        # There is probably a better fix for this.
181
        try:
1✔
182
            REQUEST.RESPONSE.notFoundError("%s\n%s" % (name, method))
1✔
183
        except AttributeError:
1✔
184
            raise KeyError(name)
1✔
185

186
    def ZopeTime(self, *args):
1✔
187
        """Utility function to return current date/time"""
188
        return DateTime(*args)
1✔
189

190
    @security.protected(view_management_screens)
1✔
191
    def ZopeVersion(self, major=False):
1✔
192
        """Utility method to return the Zope version
193

194
        Restricted to ZMI to prevent information disclosure
195
        """
196
        zversion = getZopeVersion()
1✔
197
        if major:
1✔
198
            return zversion.major
1✔
199
        else:
200
            version = '%s.%s.%s' % (zversion.major,
1✔
201
                                    zversion.minor,
202
                                    zversion.micro)
203
            if zversion.status:
1!
204
                version += '.%s%s' % (zversion.status, zversion.release)
1✔
205

206
            return version
1✔
207

208
    def DELETE(self, REQUEST, RESPONSE):
1✔
209
        """Delete a resource object."""
210
        self.dav__init(REQUEST, RESPONSE)
×
211
        raise Forbidden('This resource cannot be deleted.')
×
212

213
    def MOVE(self, REQUEST, RESPONSE):
1✔
214
        """Move a resource to a new location."""
215
        self.dav__init(REQUEST, RESPONSE)
×
216
        raise Forbidden('This resource cannot be moved.')
×
217

218
    def absolute_url(self, relative=0):
1✔
219
        """The absolute URL of the root object is BASE1 or "/".
220
        """
221
        if relative:
1✔
222
            return ''
1✔
223
        try:
1✔
224
            # Take advantage of computed URL cache
225
            return self.REQUEST['BASE1']
1✔
226
        except (AttributeError, KeyError):
×
227
            return '/'
×
228

229
    def absolute_url_path(self):
1✔
230
        """The absolute URL path of the root object is BASEPATH1 or "/".
231
        """
232
        try:
1✔
233
            return self.REQUEST['BASEPATH1'] or '/'
1✔
234
        except (AttributeError, KeyError):
×
235
            return '/'
×
236

237
    def virtual_url_path(self):
1✔
238
        """The virtual URL path of the root object is empty.
239
        """
240
        return ''
1✔
241

242
    def getPhysicalRoot(self):
1✔
243
        return self
1✔
244

245
    def getPhysicalPath(self):
1✔
246
        # Get the physical path of the object.
247
        #
248
        # Returns a path (an immutable sequence of strings) that can be used to
249
        # access this object again later, for example in a copy/paste
250
        # operation.  getPhysicalRoot() and getPhysicalPath() are designed to
251
        # operate together.
252
        #
253
        # We're at the base of the path.
254
        return ('', )
1✔
255

256

257
InitializeClass(Application)
1✔
258

259

260
def initialize(app):
1✔
261
    initializer = AppInitializer(app)
1✔
262
    initializer.initialize()
1✔
263

264

265
class AppInitializer(object):
1✔
266
    """ Initialize an Application object (called at startup) """
267

268
    def __init__(self, app):
1✔
269
        self.app = (app,)
1✔
270

271
    def getApp(self):
1✔
272
        # this is probably necessary, but avoid acquisition anyway
273
        return self.app[0]
1✔
274

275
    def commit(self, note):
1✔
276
        transaction.get().note(note)
1✔
277
        transaction.commit()
1✔
278

279
    def initialize(self):
1✔
280
        # make sure to preserve relative ordering of calls below.
281
        self.install_app_manager()
1✔
282
        self.install_required_roles()
1✔
283
        self.install_inituser()
1✔
284
        self.install_products()
1✔
285
        self.install_standards()
1✔
286
        self.install_virtual_hosting()
1✔
287
        self.install_root_view()
1✔
288

289
    def install_app_manager(self):
1✔
290
        global APP_MANAGER
291
        APP_MANAGER = ApplicationManager()
1✔
292

293
        # Remove persistent Control Panel.
294
        app = self.getApp()
1✔
295
        app._p_activate()
1✔
296

297
        if 'Control_Panel' in list(app.__dict__.keys()):
1!
298
            del app.__dict__['Control_Panel']
×
299
            app._objects = tuple(i for i in app._objects
×
300
                                 if i['id'] != 'Control_Panel')
301
            self.commit(u'Removed persistent Control_Panel')
×
302

303
    def install_required_roles(self):
1✔
304
        app = self.getApp()
1✔
305

306
        # Ensure that Owner role exists.
307
        if hasattr(app, '__ac_roles__') and not ('Owner' in app.__ac_roles__):
1!
308
            app.__ac_roles__ = app.__ac_roles__ + ('Owner',)
×
309
            self.commit(u'Added Owner role')
×
310

311
        # ensure the Authenticated role exists.
312
        if hasattr(app, '__ac_roles__'):
1!
313
            if 'Authenticated' not in app.__ac_roles__:
1!
314
                app.__ac_roles__ = app.__ac_roles__ + ('Authenticated',)
×
315
                self.commit(u'Added Authenticated role')
×
316

317
    def install_inituser(self):
1✔
318
        app = self.getApp()
1✔
319
        # Install the initial user.
320
        if hasattr(app, 'acl_users'):
1!
321
            users = app.acl_users
1✔
322
            if hasattr(users, '_createInitialUser'):
1!
323
                app.acl_users._createInitialUser()
1✔
324
                self.commit(u'Created initial user')
1✔
325
            users = aq_base(users)
1✔
326
            migrated = getattr(users, '_ofs_migrated', False)
1✔
327
            if not migrated:
1!
328
                klass = users.__class__
×
329
                from OFS.userfolder import UserFolder
×
330
                if klass is UserFolder:
×
331
                    # zope.deferredimport does a thourough job, so the class
332
                    # looks like it's from the new location already. And we
333
                    # don't want to migrate any custom user folders here.
334
                    users.__class__ = UserFolder
×
335
                    users._ofs_migrated = True
×
336
                    users._p_changed = True
×
337
                    app._p_changed = True
×
338
                    transaction.get().note(u'Migrated user folder')
×
339
                    transaction.commit()
×
340

341
    def install_virtual_hosting(self):
1✔
342
        app = self.getApp()
1✔
343
        if 'virtual_hosting' not in app:
1!
344
            from Products.SiteAccess.VirtualHostMonster import \
1✔
345
                VirtualHostMonster
346
            any_vhm = [obj for obj in app.values()
1✔
347
                       if isinstance(obj, VirtualHostMonster)]
348
            if not any_vhm:
1!
349
                vhm = VirtualHostMonster()
1✔
350
                vhm.id = 'virtual_hosting'
1✔
351
                vhm.addToContainer(app)
1✔
352
                self.commit(u'Added virtual_hosting')
1✔
353

354
    def install_root_view(self):
1✔
355
        app = self.getApp()
1✔
356
        if 'index_html' not in app:
1!
357
            from Products.PageTemplates.ZopePageTemplate import \
1✔
358
                ZopePageTemplate
359
            root_pt = ZopePageTemplate('index_html')
1✔
360
            root_pt.pt_setTitle(u'Auto-generated default page')
1✔
361
            app._setObject('index_html', root_pt)
1✔
362
            self.commit(u'Added default view for root object')
1✔
363

364
    def install_products(self):
1✔
365
        return install_products(self.getApp())
1✔
366

367
    def install_standards(self):
1✔
368
        app = self.getApp()
1✔
369
        if getattr(app, '_standard_objects_have_been_added', None) is not None:
1!
370
            delattr(app, '_standard_objects_have_been_added')
×
371
        if getattr(app, '_initializer_registry', None) is not None:
1!
372
            delattr(app, '_initializer_registry')
×
373
        transaction.get().note(u'Removed unused application attributes.')
1✔
374
        transaction.commit()
1✔
375

376

377
def install_products(app=None):
1✔
378
    folder_permissions = get_folder_permissions()
1✔
379
    meta_types = []
1✔
380
    done = {}
1✔
381
    for priority, product_name, index, product_dir in get_products():
1✔
382
        # For each product, we will import it and try to call the
383
        # intialize() method in the product __init__ module. If
384
        # the method doesnt exist, we put the old-style information
385
        # together and do a default initialization.
386
        if product_name in done:
1!
387
            continue
×
388
        done[product_name] = 1
1✔
389
        install_product(app, product_dir, product_name, meta_types,
1✔
390
                        folder_permissions)
391

392
    # Delayed install of packages-as-products
393
    for module, init_func in tuple(get_packages_to_initialize()):
1✔
394
        install_package(app, module, init_func)
1✔
395

396
    Products.meta_types = Products.meta_types + tuple(meta_types)
1✔
397
    InitializeClass(Folder.Folder)
1✔
398

399

400
def _is_package(product_dir, product_name):
1✔
401
    package_dir = os.path.join(product_dir, product_name)
1✔
402
    if not os.path.isdir(package_dir):
1✔
403
        return False
1✔
404

405
    init_py = os.path.join(package_dir, '__init__.py')
1✔
406
    if not os.path.exists(init_py) and \
1✔
407
       not os.path.exists(init_py + 'c') and \
408
       not os.path.exists(init_py + 'o'):
409
        return False
1✔
410
    return True
1✔
411

412

413
def get_products():
1✔
414
    """ Return a list of tuples in the form:
415
    [(priority, dir_name, index, base_dir), ...] for each Product directory
416
    found, sort before returning """
417
    products = []
1✔
418
    i = 0
1✔
419
    for product_dir in Products.__path__:
1✔
420
        product_names = os.listdir(product_dir)
1✔
421
        for product_name in product_names:
1✔
422
            if _is_package(product_dir, product_name):
1✔
423
                # i is used as sort ordering in case a conflict exists
424
                # between Product names.  Products will be found as
425
                # per the ordering of Products.__path__
426
                products.append((0, product_name, i, product_dir))
1✔
427
        i = i + 1
1✔
428
    products.sort()
1✔
429
    return products
1✔
430

431

432
def import_products():
1✔
433
    done = {}
1✔
434
    for priority, product_name, index, product_dir in get_products():
1✔
435
        if product_name in done:
1!
436
            LOG.warning(
×
437
                'Duplicate Product name: '
438
                'After loading Product %r from %r, '
439
                'I skipped the one in %r.' % (
440
                    product_name, done[product_name], product_dir))
441
            continue
×
442
        done[product_name] = product_dir
1✔
443
        import_product(product_dir, product_name)
1✔
444
    return list(done.keys())
1✔
445

446

447
def import_product(product_dir, product_name, raise_exc=None):
1✔
448
    if not _is_package(product_dir, product_name):
1!
449
        return
×
450

451
    global_dict = globals()
1✔
452
    product = __import__("Products.%s" % product_name,
1✔
453
                         global_dict, global_dict, ('__doc__', ))
454
    if hasattr(product, '__module_aliases__'):
1!
455
        for k, v in product.__module_aliases__:
×
456
            if k not in sys.modules:
×
457
                if isinstance(v, str) and v in sys.modules:
×
458
                    v = sys.modules[v]
×
459
                sys.modules[k] = v
×
460

461

462
def get_folder_permissions():
1✔
463
    folder_permissions = {}
1✔
464
    for p in Folder.Folder.__ac_permissions__:
1!
465
        permission, names = p[:2]
×
466
        folder_permissions[permission] = names
×
467
    return folder_permissions
1✔
468

469

470
def install_product(app, product_dir, product_name, meta_types,
1✔
471
                    folder_permissions, raise_exc=None):
472
    if not _is_package(product_dir, product_name):
1!
473
        return
×
474

475
    __traceback_info__ = product_name
1✔
476
    global_dict = globals()
1✔
477
    product = __import__("Products.%s" % product_name,
1✔
478
                         global_dict, global_dict, ('__doc__', ))
479

480
    # Install items into the misc_ namespace, used by products
481
    # and the framework itself to store common static resources
482
    # like icon images.
483
    misc_ = pgetattr(product, 'misc_', {})
1✔
484
    if misc_:
1!
485
        if isinstance(misc_, dict):
×
486
            misc_ = Misc_(product_name, misc_)
×
487
        setattr(Application.misc_, product_name, misc_)
×
488

489
    productObject = FactoryDispatcher.Product(product_name)
1✔
490
    context = ProductContext(productObject, app, product)
1✔
491

492
    # Look for an 'initialize' method in the product.
493
    initmethod = pgetattr(product, 'initialize', None)
1✔
494
    if initmethod is not None:
1✔
495
        initmethod(context)
1✔
496

497

498
def install_package(app, module, init_func, raise_exc=None):
1✔
499
    """Installs a Python package like a product."""
500
    name = module.__name__
1✔
501
    product = FactoryDispatcher.Product(name)
1✔
502
    product.package_name = name
1✔
503

504
    if init_func is not None:
1!
505
        newContext = ProductContext(product, app, module)
1✔
506
        init_func(newContext)
1✔
507

508
    package_initialized(module, init_func)
1✔
509

510

511
def pgetattr(product, name, default=install_products, __init__=0):
1✔
512
    if not __init__ and hasattr(product, name):
1✔
513
        return getattr(product, name)
1✔
514
    if hasattr(product, '__init__'):
1!
515
        product = product.__init__
1✔
516
        if hasattr(product, name):
1!
517
            return getattr(product, name)
×
518

519
    if default is not install_products:
1!
520
        return default
1✔
521

522
    raise AttributeError(name)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc