Skip to content

Commit 290ef02

Browse files
committed
issue2550864 - Potential information leakage via journal/history
Fix this by making the hyperdb::Class::history function check for view permissions on the journaled properties. So a user that sees [hidden] for a property in the web interface doesn;t see the property changes in the history. While doing this, relocated the filter for quiet properties from the templating class to the hyperdb. Also added the skipquiet option to the history command in roundup-admin.py to enable filtering of quiet params. Also changed calls to history() in the backend databases to report all items. Changed inline documentation for all history calls that document the actions. The create action (before nov 6 2002) used to record all parameters. After that point the create call uses an empty dictionary. The filtering code depends on the create dictionary being empty. It may not operate properly on very old roundup databases. Changed calls to logging.getLogger to roundup.hyperdb.backends to allow filtering the back end while keeping hyperdb logging. In cgi/templating.py, changed history() function consolidating handiling of link and unlink actions Added tests for quiet property filtering and permission filtering of history.
1 parent 2b4bdcf commit 290ef02

File tree

9 files changed

+326
-68
lines changed

9 files changed

+326
-68
lines changed

CHANGES.txt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,12 @@ Features:
105105
prop.quiet=True
106106
support for anydb backend, added tests, doc updates, support for
107107
ignoring quiet setting using showall=True in call to history()
108-
function in templates by John Rouillard.
108+
function in templates by (John Rouillard). (Note implementation
109+
changed while implementing fix for issue2550864. Filtering of
110+
quiet properties pushed down to the hyperdb.py::Class::history
111+
function. This fixes a small bug in the implementation that caused
112+
a limiting the templating history call to display fewer than the
113+
the requested number of items if some were quiet.)
109114
- issue2550767: Add newitemcopy.py detector to notify users of new
110115
items. Added to detectors directory and a README.txt generated to
111116
describe the purpose of the directory. It also says the detectors
@@ -196,6 +201,12 @@ Features:
196201
genconfig but it uses values from an existing config.ini
197202
rather than default values. Use to update an existing
198203
config.ini with new options and help text. (John Rouillard)
204+
- issue2550864: Potential information leakage via journal/history
205+
Hyperdb history function now only returns properties that the user
206+
has View permissions on. Can be overridden by setting a parameter
207+
when calling the method. Also restructured code that implemented
208+
issue1714899 moving it from the templating class to the hyperdb.
209+
(John Rouillard)
199210

200211
Fixed:
201212

doc/customizing.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2461,6 +2461,8 @@ history render the journal of the current item as
24612461
HTML. By default properties marked as "quiet" (see
24622462
`design documentation`_) are not shown unless the
24632463
function is called with the showall=True parameter.
2464+
Properties that are not Viewable to the user are not
2465+
shown.
24642466
renderQueryForm specific to the "query" class - render the search form
24652467
for the query
24662468
hasPermission specific to the "user" class - determine whether the
@@ -3213,6 +3215,13 @@ journal. This is generally generated with the template::
32133215

32143216
<tal:block tal:replace="structure context/history" />
32153217

3218+
or::
3219+
3220+
<tal:block
3221+
tal:replace="structure python:context.history(showall=True)" />
3222+
3223+
if you want to show history entries for quiet properties.
3224+
32163225
*To be done:*
32173226

32183227
*The actual history entries of the item may be accessed for manual

doc/design.txt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -286,10 +286,12 @@ E.G. assuming title is part of an Issue::
286286
will create a property called ``title`` that will be included in the
287287
get_required_props() output. Calling
288288
db.issue.properties['title'].get_default_value() will return "not set".
289-
Changes to the property will not be displayed in emailed change notes,
290-
the history at the end of the item pages in the web interface and will
291-
be suppressed in the confirmation notice (displayed as a green banner)
292-
shown on changes.
289+
Changes to the property will not be displayed in:
290+
291+
- emailed change notes,
292+
- the history at the end of the item pages in the web interface
293+
- in the confirmation notice (displayed as a green banner)
294+
shown on changes.
293295

294296
These objects are used when specifying what properties belong in classes::
295297

roundup/admin.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,23 +1012,34 @@ def do_table(self, args):
10121012
return 0
10131013

10141014
def do_history(self, args):
1015-
''"""Usage: history designator
1015+
''"""Usage: history designator [skipquiet]
10161016
Show the history entries of a designator.
10171017
10181018
A designator is a classname and a nodeid concatenated,
10191019
eg. bug1, user10, ...
10201020
1021-
Lists the journal entries for the node identified by the designator.
1021+
Lists the journal entries viewable by the user for the
1022+
node identified by the designator. If skipquiet is the
1023+
second argument, journal entries for quiet properties
1024+
are not shown.
10221025
"""
1026+
10231027
if len(args) < 1:
10241028
raise UsageError(_('Not enough arguments supplied'))
10251029
try:
10261030
classname, nodeid = hyperdb.splitDesignator(args[0])
10271031
except hyperdb.DesignatorError, message:
10281032
raise UsageError(message)
10291033

1034+
skipquiet = False
1035+
if len(args) == 2:
1036+
if args[1] != 'skipquiet':
1037+
raise UsageError("Second argument is not skipquiet")
1038+
skipquiet = True
1039+
10301040
try:
1031-
print self.db.getclass(classname).history(nodeid)
1041+
print self.db.getclass(classname).history(nodeid,
1042+
skipquiet=skipquiet)
10321043
except KeyError:
10331044
raise UsageError(_('no such class "%(classname)s"')%locals())
10341045
except IndexError:

roundup/backends/back_anydbm.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ def getclass(self, classname):
274274
def clear(self):
275275
"""Delete all database contents
276276
"""
277-
logging.getLogger('roundup.hyperdb').info('clear')
277+
logging.getLogger('roundup.hyperdb.backend').info('clear')
278278
for cn in self.classes:
279279
for dummy in 'nodes', 'journals':
280280
path = os.path.join(self.dir, 'journals.%s'%cn)
@@ -322,7 +322,7 @@ def opendb(self, name, mode):
322322
# whichdb() function to do this
323323
if not db_type or hasattr(anydbm, 'whichdb'):
324324
if __debug__:
325-
logging.getLogger('roundup.hyperdb').debug(
325+
logging.getLogger('roundup.hyperdb.backend').debug(
326326
"opendb anydbm.open(%r, 'c')"%path)
327327
return anydbm.open(path, 'c')
328328

@@ -334,7 +334,7 @@ def opendb(self, name, mode):
334334
raise hyperdb.DatabaseError(_("Couldn't open database - the "
335335
"required module '%s' is not available")%db_type)
336336
if __debug__:
337-
logging.getLogger('roundup.hyperdb').debug(
337+
logging.getLogger('roundup.hyperdb.backend').debug(
338338
"opendb %r.open(%r, %r)"%(db_type, path, mode))
339339
return dbm.open(path, mode)
340340

@@ -395,7 +395,7 @@ def savenode(self, classname, nodeid, node):
395395
""" perform the saving of data specified by the set/addnode
396396
"""
397397
if __debug__:
398-
logging.getLogger('roundup.hyperdb').debug(
398+
logging.getLogger('roundup.hyperdb.backend').debug(
399399
'save %s%s %r'%(classname, nodeid, node))
400400
self.transactions.append((self.doSaveNode, (classname, nodeid, node)))
401401

@@ -409,15 +409,15 @@ def getnode(self, classname, nodeid, db=None, cache=1):
409409
cache_dict = self.cache.setdefault(classname, {})
410410
if nodeid in cache_dict:
411411
if __debug__:
412-
logging.getLogger('roundup.hyperdb').debug(
412+
logging.getLogger('roundup.hyperdb.backend').debug(
413413
'get %s%s cached'%(classname, nodeid))
414414
self.stats['cache_hits'] += 1
415415
return cache_dict[nodeid]
416416

417417
if __debug__:
418418
self.stats['cache_misses'] += 1
419419
start_t = time.time()
420-
logging.getLogger('roundup.hyperdb').debug(
420+
logging.getLogger('roundup.hyperdb.backend').debug(
421421
'get %s%s'%(classname, nodeid))
422422

423423
# get from the database and save in the cache
@@ -450,7 +450,7 @@ def destroynode(self, classname, nodeid):
450450
"""Remove a node from the database. Called exclusively by the
451451
destroy() method on Class.
452452
"""
453-
logging.getLogger('roundup.hyperdb').info(
453+
logging.getLogger('roundup.hyperdb.backend').info(
454454
'destroy %s%s'%(classname, nodeid))
455455

456456
# remove from cache and newnodes if it's there
@@ -564,15 +564,17 @@ def addjournal(self, classname, nodeid, action, params, creator=None,
564564
""" Journal the Action
565565
'action' may be:
566566
567-
'create' or 'set' -- 'params' is a dictionary of property values
567+
'set' -- 'params' is a dictionary of property values
568+
'create' -- 'params' is an empty dictionary as of
569+
Wed Nov 06 11:38:43 2002 +0000
568570
'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
569-
'retire' -- 'params' is None
571+
'retired' or 'restored' -- 'params' is None
570572
571573
'creator' -- the user performing the action, which defaults to
572574
the current user.
573575
"""
574576
if __debug__:
575-
logging.getLogger('roundup.hyperdb').debug(
577+
logging.getLogger('roundup.hyperdb.backend').debug(
576578
'addjournal %s%s %s %r %s %r'%(classname,
577579
nodeid, action, params, creator, creation))
578580
if creator is None:
@@ -583,7 +585,7 @@ def addjournal(self, classname, nodeid, action, params, creator=None,
583585
def setjournal(self, classname, nodeid, journal):
584586
"""Set the journal to the "journal" list."""
585587
if __debug__:
586-
logging.getLogger('roundup.hyperdb').debug(
588+
logging.getLogger('roundup.hyperdb.backend').debug(
587589
'setjournal %s%s %r'%(classname, nodeid, journal))
588590
self.transactions.append((self.doSetJournal, (classname, nodeid,
589591
journal)))
@@ -685,7 +687,7 @@ def pack(self, pack_before):
685687
packed += 1
686688
db[key] = marshal.dumps(l)
687689

688-
logging.getLogger('roundup.hyperdb').info(
690+
logging.getLogger('roundup.hyperdb.backend').info(
689691
'packed %d %s items'%(packed, classname))
690692

691693
if db_type == 'gdbm':
@@ -708,7 +710,7 @@ def commit(self, fail_ok=False):
708710
709711
The only backend this seems to affect is postgres.
710712
"""
711-
logging.getLogger('roundup.hyperdb').info('commit %s transactions'%(
713+
logging.getLogger('roundup.hyperdb.backend').info('commit %s transactions'%(
712714
len(self.transactions)))
713715

714716
# keep a handle to all the database files opened
@@ -833,7 +835,7 @@ def doDestroyNode(self, classname, nodeid):
833835
def rollback(self):
834836
""" Reverse all actions from the current transaction.
835837
"""
836-
logging.getLogger('roundup.hyperdb').info('rollback %s transactions'%(
838+
logging.getLogger('roundup.hyperdb.backend').info('rollback %s transactions'%(
837839
len(self.transactions)))
838840

839841
for method, args in self.transactions:
@@ -2098,7 +2100,8 @@ def export_journals(self):
20982100
properties = self.getprops()
20992101
r = []
21002102
for nodeid in self.getnodeids():
2101-
for nodeid, date, user, action, params in self.history(nodeid):
2103+
for nodeid, date, user, action, params in self.history(nodeid,
2104+
enforceperm=False, skipquiet=False):
21022105
date = date.get_tuple()
21032106
if action == 'set':
21042107
export_data = {}

roundup/backends/rdbms_common.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,7 @@ def update_class(self, spec, old_spec, force=0):
533533
if not self.config.RDBMS_ALLOW_ALTER:
534534
raise DatabaseError(_('ALTER operation disallowed: %r -> %r.'%(old_spec, new_spec)))
535535

536-
logger = logging.getLogger('roundup.hyperdb')
536+
logger = logging.getLogger('roundup.hyperdb.backend')
537537
logger.info('update_class %s'%spec.classname)
538538

539539
logger.debug('old_spec %r'%(old_spec,))
@@ -858,7 +858,7 @@ def clear(self):
858858
Note: I don't commit here, which is different behaviour to the
859859
"nuke from orbit" behaviour in the dbs.
860860
"""
861-
logging.getLogger('roundup.hyperdb').info('clear')
861+
logging.getLogger('roundup.hyperdb.backend').info('clear')
862862
for cn in self.classes:
863863
sql = 'delete from _%s'%cn
864864
self.sql(sql)
@@ -1194,7 +1194,7 @@ def destroynode(self, classname, nodeid):
11941194
"""Remove a node from the database. Called exclusively by the
11951195
destroy() method on Class.
11961196
"""
1197-
logging.getLogger('roundup.hyperdb').info('destroynode %s%s'%(
1197+
logging.getLogger('roundup.hyperdb.backend').info('destroynode %s%s'%(
11981198
classname, nodeid))
11991199

12001200
# make sure the node exists
@@ -1263,9 +1263,14 @@ def addjournal(self, classname, nodeid, action, params, creator=None,
12631263
""" Journal the Action
12641264
'action' may be:
12651265
1266-
'create' or 'set' -- 'params' is a dictionary of property values
1266+
'set' -- 'params' is a dictionary of property values
1267+
'create' -- 'params' is an empty dictionary as of
1268+
Wed Nov 06 11:38:43 2002 +0000
12671269
'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
1268-
'retire' -- 'params' is None
1270+
'retired' or 'restored' -- 'params' is None
1271+
1272+
'creator' -- the user performing the action, which defaults to
1273+
the current user.
12691274
"""
12701275
# handle supply of the special journalling parameters (usually
12711276
# supplied on importing an existing database)
@@ -1409,7 +1414,7 @@ def pack(self, pack_before):
14091414
def sql_commit(self, fail_ok=False):
14101415
""" Actually commit to the database.
14111416
"""
1412-
logging.getLogger('roundup.hyperdb').info('commit')
1417+
logging.getLogger('roundup.hyperdb.backend').info('commit')
14131418

14141419
self.conn.commit()
14151420

@@ -1455,7 +1460,7 @@ def rollback(self):
14551460
Undo all the changes made since the database was opened or the last
14561461
commit() or rollback() was performed.
14571462
"""
1458-
logging.getLogger('roundup.hyperdb').info('rollback')
1463+
logging.getLogger('roundup.hyperdb.backend').info('rollback')
14591464

14601465
self.sql_rollback()
14611466

@@ -1470,7 +1475,7 @@ def rollback(self):
14701475
self.clearCache()
14711476

14721477
def sql_close(self):
1473-
logging.getLogger('roundup.hyperdb').info('close')
1478+
logging.getLogger('roundup.hyperdb.backend').info('close')
14741479
self.conn.close()
14751480

14761481
def close(self):
@@ -2955,7 +2960,8 @@ def export_journals(self):
29552960
properties = self.getprops()
29562961
r = []
29572962
for nodeid in self.getnodeids():
2958-
for nodeid, date, user, action, params in self.history(nodeid):
2963+
for nodeid, date, user, action, params in self.history(nodeid,
2964+
enforceperm=False, skipquiet=False):
29592965
date = date.get_tuple()
29602966
if action == 'set':
29612967
export_data = {}

roundup/cgi/templating.py

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -973,7 +973,7 @@ def history(self, direction='descending', dre=re.compile('^\d+$'),
973973
classname, id, current[prop_n])
974974

975975
# get the journal, sort and reverse
976-
history = self._klass.history(self._nodeid)
976+
history = self._klass.history(self._nodeid, skipquiet=(not showall))
977977
history.sort()
978978
history.reverse()
979979

@@ -987,22 +987,13 @@ def history(self, direction='descending', dre=re.compile('^\d+$'),
987987
for id, evt_date, user, action, args in history:
988988
date_s = str(evt_date.local(timezone)).replace("."," ")
989989
arg_s = ''
990-
if action == 'link' and type(args) == type(()):
990+
if action in ['link', 'unlink'] and type(args) == type(()):
991991
if len(args) == 3:
992992
linkcl, linkid, key = args
993993
arg_s += '<a rel="nofollow" href="%s%s">%s%s %s</a>'%(linkcl, linkid,
994994
linkcl, linkid, key)
995995
else:
996996
arg_s = str(args)
997-
998-
elif action == 'unlink' and type(args) == type(()):
999-
if len(args) == 3:
1000-
linkcl, linkid, key = args
1001-
arg_s += '<a rel="nofollow" href="%s%s">%s%s %s</a>'%(linkcl, linkid,
1002-
linkcl, linkid, key)
1003-
else:
1004-
arg_s = str(args)
1005-
1006997
elif type(args) == type({}):
1007998
cell = []
1008999
for k in args.keys():
@@ -1041,10 +1032,7 @@ def history(self, direction='descending', dre=re.compile('^\d+$'),
10411032
except NoTemplate:
10421033
hrefable = 0
10431034

1044-
if (not showall) and prop.quiet:
1045-
# Omit quiet properties from history/changelog
1046-
continue
1047-
elif isinstance(prop, hyperdb.Multilink) and args[k]:
1035+
if isinstance(prop, hyperdb.Multilink) and args[k]:
10481036
ml = []
10491037
for linkid in args[k]:
10501038
if isinstance(linkid, type(())):

0 commit comments

Comments
 (0)