.. meta::
:description:
Critical documentation for upgrading the Roundup Issue
Tracker. Actions that must be taken when upgrading from
one version to another are documented here.
.. index:: Upgrading
======================================
Upgrading to newer versions of Roundup
======================================
Please read each section carefully and edit the files in your tracker home
accordingly. Note that there is information about upgrade procedures in the
`administration guide`_ in the `Software Upgrade`_ section.
If a specific version transition isn't mentioned here (e.g. 0.6.7 to
0.6.8) then you don't need to do anything. If you're upgrading from
0.5.6 to 0.6.8 though, you'll need to apply the "0.5 to 0.6" and
"0.6.x to 0.6.3" steps.
General steps:
1. Make note of your current Roundup version.
2. Take your Roundup installation offline (web, email,
cron scripts, roundup-admin etc.)
3. Backup your Roundup instance
4. Install the new version of Roundup (preferably in a new virtual
environment)
5. Make version specific changes as described below for
each version transition. If you are starting at 1.5.0
and installing to 2.3.0, you need to make the changes for **all**
versions starting at 1.5 and ending at 2.3. E.G.
1.5.0 -> 1.5.1, 1.5.1 -> 1.6.0, ..., 2.1.0 -> 2.2.0,
2.2.0 -> 2.3.0.
6. Run ``roundup-admin -i migrate`` using
the newer version of Roundup for the instance you are
upgrading. This will update the database if it is
required.
7. Bring your Roundup instance back online
8. Test
Repeat for each tracker instance.
.. note:: The v1.5.x releases of Roundup were the last to support
Python v2.5 and v2.6. Starting with the v1.6 releases of Roundup
Python version 2.7 that is newer than 2.7.2 is required to run
Roundup. Starting with Roundup version 2.0.0 we also support
Python 3 versions newer than 3.6. Roundup version 2.5 supports
Python 3.7 and newer. Roundup version 2.6 supports Python version
3.10 and newer.
Recent release notes have the following labels:
* **required** - Roundup will not work properly if these steps are not done
* **recommended** - Roundup will still work, but these steps can cause
security or stability issues if not done.
* **optional** - new features or changes to existing features you might
want to use
* **info** - important possibly visible changes in how things operate
* **obsolete** - ignore these entries. They are no longer required.
If you use virtual environments for your installation, you
can run trackers with different versions of Roundup. So you
can have one tracker using version 2.2.0 and another tracker
using version 1.6.1. This allows you to upgrade trackers one
at a time rather than having to upgrade all your trackers at
once. Note that downgrading may require restoring your
database to an earlier version, so make sure you backed up
your database.
.. note::
This file only includes versions released in the last 10
years. If you are upgrading from an older version, start with the
changes in the `historical migration `_
document.
.. admonition:: Python 2 Support
If you are running Roundup under Python 2, you should make plans to
switch to Python 3. Release 2.4.0 (Jul 2024) is the last release to
officially support Python 2. The next non-patch release scheduled
for 2025 will mark 5 years since Roundup supported Python 3.
.. admonition:: XHTML Support Deprecation Notice
If you are running a tracker where the ``html_version`` setting in
``config.ini`` is ``xhtml``, you should plan to change your
templates to use html (HTML5). If you are affected by this, please
send email to the roundup-users mailing list (roundup-users at
lists.sourceforge.net). Version 2.3.0 is the last version to support
XHTML.
.. raw:: html
Contents:
.. contents::
:local:
.. raw:: html
.. index:: Upgrading; 2.5.0 to 2.6.0
Migrating from 2.5.0 to 2.6.0
=============================
Incomplete CSRF protection fixed (required)
--------------------------------------------
This release fixes cross site request forgery (CSRF) protection for
the PATCH method. You do not have to change anything if you are
running release 2.6.0 or newer. This bug was introduced in version
2.0.0 when the REST interface that implements the PATCH method was
added.
The ``PATCH`` method is used with the rest interface and allows
partial modification of a Roundup object/document. If you have REST
turned off for your tracker, you are not vulnerable.
This release also returns a 405 Method not allowed error for all
methods except GET, HEAD, and POST when querying the main (HTML)
interface. Previously, other methods acted like GET was specified.
.. _CVE-2026-csrfL:
Remediating missing PATCH CSRF protections
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you are using the REST API, there are two ways to fix this issue
with releases 2.0.0 through 2.5.0:
1. if you are not using PATCH in your workflow, disable the PATCH
method in your tracker,
2. patch the cgi/client.py file to include the PATCH method in csrf
protections.
Disable PATCH
^^^^^^^^^^^^^
To disable the PATCH method for your tracker add the following code to
the file ``interfaces.py`` in your tracker's home directory::
from roundup.cgi.client import UsageError
from roundup.cgi.client import Client
original_handle_csrf = Client.handle_csrf
def csrf_wrapper_deny_patch(self, api=False):
method = self.env['REQUEST_METHOD']
if method == 'PATCH':
raise UsageError("Blocked Request: %s" % method)
return original_handle_csrf(self, api)
Client.handle_csrf = csrf_wrapper_deny_patch
then restart your tracker. You can test it using::
curl -u user:password -X PATCH \
-H "Origin: " /rest/data/issue/1
and you should receive::
{ "error": { "status": 400, "msg": "Blocked Request: PATCH"}}
This will disable the use of PATCH in all web requests. When you
upgrade to release 2.6, remember to re-enable ``PATCH`` you can
re-enable it by deleting the code above from ``interfaces.py``.
Apply CSRF Protections
^^^^^^^^^^^^^^^^^^^^^^
However, if you need to use PATCH request, you can change the
installed Roundup source and apply CSRF protections to the PATCH
command.
Make a backup copy of the file before changing it. Find the lines in
cgi/client.py that look like::
# Assume: never allow changes via GET
if method not in ['POST', 'PUT', 'DELETE']:
if (self.form.list is not None) and ("@csrf" in self.form):
Change the middle line so it looks like::
if method not in ['POST', 'PUT', 'DELETE', 'PATCH']:
.. comment: end of CVE include marker
Correct property permission check in history display (recommended)
------------------------------------------------------------------
If you are upgrading to 2.6, you don't have to do anything and you can
skip this section.
If you are running versions 1.6.0 to version 2.5 the filtering of
history/journal items does not work correctly if the property is
protected by a check function.
.. _CVE-2026-history_propL:
Remediating incomplete property permission check
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To fix this, you need to find the ``View`` and ``Edit`` checks in the
history method in the ``roundup/hyperdb.py`` file. Make a backup copy
of the file before making a change. (Note that the 1.6 releases only
checked for ``View`` permissions.) In the 1.6 release it looks
something like::
if enforceperm and not perm("View",
self.db.getuid(),
self.classname,
property=k ):
logger.debug("skipping unViewable property %s in >
k, j_repr)
del j[4][k]
continue
Add::
itemid=nodeid
to the View (and Edit check if present) so it looks like::
if enforceperm and not perm("View",
self.db.getuid(),
self.classname,
itemid=nodeid,
property=k )
In the 2.5 and newer releases this code block includes a check for
Edit permissions and when fixed looks like::
if enforceperm and not (perm("View",
uid,
self.classname,
itemid=nodeid,
property=key) or
perm("Edit",
uid,
self.classname,
itemid=nodeid,
property=key)):
logger.debug("skipping unaccessible property "
"%s::%s seen by user%s in %s",
self.classname, key, uid, j_repr)
del args[key]
continue
.. comment: end of CVE include marker
Default Logs Include Unique Request Identifier (info)
-----------------------------------------------------
The default logging format has been changed from::
%(asctime)s %(levelname)s %(message)s
to::
%(asctime)s %(trace_id)s %(levelname)s %(message)s
So logs now look like::
2025-08-20 03:25:00,308 f6RPbT2s70vvJ2jFb9BQNF DEBUG get user1 cached
which in the previous format would look like::
2025-08-20 03:25:00,308 DEBUG get user1 cached
The new format includes ``trace_id`` which is a thread and process
unique identifier for a single request. So you can link together all
of the log lines and determine where a slow down or other
problem occurred.
The logging format is now a ``config.ini`` parameter in the
``logging`` section with the name ``format``. You can change it if you
would like the old logging format without having to create a logging
configuration file. See :ref:`rounduplogging` for details.
Make Pagination Links Keep Search Name (optional)
-------------------------------------------------
When displaying a named search, index templates don't preserve
the name when using the pagination (Next/Prev) links. This is
fixed in the 2.6.0 templates for issues/bugs/tasks. To make the
change to your templates, look for the pagination links (look for
prev or previous case insensitive) in your tracker's html
subdirectory and change::
request.indexargs_url(request.classname,
{'@startwith':prev.first, '@pagesize':prev.size})"
to read::
request.indexargs_url(request.classname,
dict({'@dispname': request.dispname}
if request.dispname
else {},
**{'@startwith':prev.first, '@pagesize':prev.size}))"
This code will be embedded in templating markup that is not shown
above. The change above is for your previous/prev link. The
change for the next pagination link is similar with::
{'@startwith':next.first, '@pagesize':next.size}
replacing::
{'@startwith':prev.first, '@pagesize':prev.size}
in the example.
This moves the existing dictionary used to override the URL
arguments to the second argument inside a ``dict()`` call. It
also adds ``**`` before it. This change creates a new override
dictionary that includes an ``@dispname`` parameter if it is set
in the request. If ``@dispname`` is not set, the existing
dictionary contents are used.
Support authorized changes in your tracker (optional)
-----------------------------------------------------
An auditor can require change verification with user's password.
When changing sensitive information (e.g. passwords) it is
useful to ask for a validated authorization. This makes sure
that the user is present by typing their password.
You can add this to your auditors using the example
:ref:`sensitive_changes`.
To use this, you must copy ``_generic.reauth.html`` into your
tracker's html subdirectory. See the classic template directory for a
copy. If you are using jinja2, see the jinja2 template directory.
Then you can raise a Reauth exception and have the proper page
displayed.
Also javascript *MUST* be turned on if this is used with a file
input. If JavaScript is not turned on, attached files are lost during
the reauth step. Information from other types of inputs (password,
date, text etc.) do not need JavaScript to work.
See :ref:`Confirming the User` in the reference manual for details.
Support for dictConfig Logging Configuration (optional)
-------------------------------------------------------
Roundup's basic log configuration via config.ini has always had the
ability to use an ini style logging configuration to set levels per
log channel, control output file rotation etc.
With Roundup 2.6 you can use a JSON like file to configure logging
using `dictConfig
`_. The
JSON file format as been enhanced to support comments that are
stripped before being processed by the logging system.
You can read about the details in the :ref:`admin manual `.
Fix user.item.html template producing invalid Javascript (optional)
-------------------------------------------------------------------
The html template ``page.html`` in the classic, devel, minimal, and
responsive tracker templates define a ``user_src_input`` macro. This
macro produces invalid javascript for the ``onblur`` event when used
by ``user.item.html``. The only effect from this bug is a javascript
error reported in the user's browser when the user does not have edit
permissions on the page. It doesn't have any user visible impact.
If you want to fix this, replace::
tal:attributes="onblur python:edit_ok and 'split_name(this)';
with::
tal:attributes="onblur python:'split_name(this)' if edit_ok else '';
in the ``html/page.html`` file in your tracker.
Allow users without a password to log in (optional)
---------------------------------------------------
You can configure a tracker to allow a login without a password.
However the default html templates require the password field to
be filled in. This prevents a login with an empty password.
If you don't want to allow a login without a password, you can
skip this section.
This change automatically removes the required attribute if the
``config.ini`` ``login_empty_passwords`` setting is enabled
(true). The default is disabled with the value ``no``).
This change is the default for the tracker templates in 2.6 and
newer.
To add this to your tracker, change the ``page.html`` (for TAL
based trackers) or ``layout/navigation.html`` (for jinja2 trackers).
For TAL trackers, replace the ``required`` parameter by finding
the following password input in the tracker's ``html/page.html``
file::
and modifying it to look like::
The equivalent change for jinja2's
``html/layout/navigation.html`` based template starts with::
and changes to::
REST interface changes (info)
-----------------------------
The default type of a message file (the msg class) is now
``text/plain``. This can be changed to a different mime type using
`the interfaces.py file`_. See `the rest documentation on getting file
content for details `_.
Roundup supports a wildcard mime type. So ``text/*`` would match any
text resource. The response to this request should have a full mime
type like ``text/plain`` or ``text/csv`` etc. However Roundup used to
return ``text/*`` which makes it difficult for the client to interpret
the data. This release should do a better job of returning a proper
subtype replacing the ``*``.
The mime type of ``application/*`` resolves to ``application/json`` by
default. Using it before resulted in an error.
Malformed mime types assigned to files/messages could cause a crash,
now they are just ignored.
There have been some internal refactorings and improvements in the
REST code that will make it a bit faster.
Make File Contents Immutable for Everybody (optional)
-----------------------------------------------------
The HTML based interface for files and messages doesn't provide a way
to change file content. However it is possible for privileged people
to modify the content via the web. In most cases, this change will not
be recorded in the audit log. It can be detected by looking at the
change time of the file. Then compare it to the change time of files
before and after it. Since files are created in order, the file
``msg50`` should have a change timestamp after ``msg49`` and before
``msg51``.
The 2.6.0 release includes an immutable_file_contents.py
detector. If you copy the detector into your tracker's detector
directory nobody, including users with admin rights, can change
file/msg contents via Roundup. Changes to files would have to be
done by logging into the Roundup server and editing the files
locally.
For non-admin user's the following edit permission for FileClass based
classes will prevent regular users from changing file content via
Roundup. Remove the existing ``Edit`` permission from your FileClass
based classes. Then add your classname to the loop. The permission
strips ``content`` from the list of editable properties and permits
editing of the other properties::
for cl in 'file', 'msg':
properties = list(x for x in
db.getclass(cl).getprops(protected=False).keys()
if x != 'content')
file_edit_perm = db.security.addPermission(
name='Edit', klass=cl,
properties=properties,
description="User is allowed to edit all %s props except content" % cl)
db.security.addPermissionToRole('User', file_edit_perm)
Enable tokenless/nonceless CSRF protection (optional)
-----------------------------------------------------
This version implements CSRF protection using `Cross-Site Request
Forgery by Filippo Valsorda `_.
This is an effective method for CSRF protection and is much
simpler as well. There are no configurable options unlike the 8
options for the older method.
It is off by default and must be manually enabled. You can enable
it on a per-tracker basis using ``config.ini``. In the ``[web]``
section of ``config.ini`` set ``use_tokenless_csrf_protection`` to
``yes`` to enable it. This also disable validation using the older
nonce and header based CSRF settings.
When enabled, calls to ``utils.anti_csrf_nonce()`` will always
return ``"0"``. As a result, you do not have to change your
templates. Performance should be better since the random nonce is
not generated nor saved to the otks database.
Additional details can be found in the :ref:`nonceless CSRF` section of
the reference guide.
Make Query Edit Page Queries Display Query Name (optional)
----------------------------------------------------------
When queries are started from the main ``Your Queries`` menu section,
the window shows the query name for TAL based trackers. Users can
pick which queries/searches appear in the main menu's ``Your Queries``
section by clicking the ``edit`` link to go to the edit screen. The
``edit`` screen also lets users run the queries that aren't in the
main section.
Before this update, the query name wasn't shown when running queries
from the ``edit`` screen. This update fixes that for all the included
templates. It also makes the jinja2 issue index template show the
query name.
You can add this to your tracker. If you are not using a jinja2 based
tracker, open your tracker's ``html/query.edit.html`` file. Find and
change the line that includes::
{{ query.name.plain() }}
with::
{{ query.name.plain() }}
Then in the ``html/issue.index.html`` you will add::
{% if request.form['@dispname'] %}
- {{ request.form['@dispname'].value|u }}
{% endif %}
to the ``head_title`` and ``page_header`` blocks so they look like::
{% block head_title %}
{% trans %}List of issues{% endtrans %}
{% if request.form['@dispname'] %}
- {{ request.form['@dispname'].value|u }}
{% endif %}
{% endblock %}
{% block page_header %}
{% trans %}List of issues{% endtrans %}
{% if request.form['@dispname'] %}
- {{ request.form['@dispname'].value|u }}
{% endif %}
{% endblock %}
Modernizing Classic Tracker HTML (optional)
-------------------------------------------
This update improves the classic tracker by changing the page layout
to use modern HTML and CSS instead of the old table layout from
2001. All the updates are done by changing the ``page.html`` and the
``style.css`` files.
Among the fixes are:
* HTML5 landmarks like header, main, search, and nav replace the
table that was used for layout.
* a meta viewport tag was added for mobile devices.
* the sidebar and main columns now use flexbox. They will stack and
turn into one column when the screen size is less than 600px.
* The sidebar will change from 6 blocks in a column to 6 blocks in a
responsive grid when the screen size is under 600px.
* Text in the nav/sidebar will have wrapped lines indented making it
easier to identify individual queries with long names.
* The other templates in your tracker should not need any changes. If
they are too wide the updated ``page.html`` enables keyboard or
touch horizontal scrolling to see all of a table or other content.
* A line now appears under a link when you hover over it.
* The header uses flexbox to arrange the left side (logo/page
description) and right side (search) components.
The most noticable change is that the page title now lines up with the
edge of the screen instead of the edge of the main column. Other than
that your tracker should look pretty much the same as before.
Two files were changed to provide this basic responsiveness:
``page.html`` and ``style.css``.
Most local changes to ``page.html`` are done to add javascript. You
should be able to copy the new page.html from the classic tracker
template and put your ``script`` tags back in to benefit from this
upgrade.
You should also copy ``style.css`` into your tracker home's html
directory and merge any local changes you made. At the top of the new
``style.css`` are directions for two adjustments. Long query names
that wrap in the sidebar will have their wrapped lines indented by
0.5em. You can control this by uncommenting and adjusting the value of
the css custom property ``--long-query-indent``. Additionally you can
change the 600px viewport breakpoint by following the directions at
the top of ``style.css``.
Jinja Template Bootstrap Dependency Updated (optional)
------------------------------------------------------
A dependency for the jinja template has been updated. This is an
upgrade to the latest 4.x stable release.
bootstrap:
Upgraded from 4.4.1 to 4.6.2
Copy the files: ``bootstrap.min.css`` and ``bootstrap.min.js`` from
the jinja2 ``static`` :term:`template` directory (meaning 3) to your
tracker's ``static`` directory.
This change did not show any issues in testing, but you might want to
hold onto your original files in case of an issue. If you experience
and issue you should report it on the `roundup-users mailing list
`_.
.. index:: Upgrading; 2.4.0 to 2.5.0
Migrating from 2.4.0 to 2.5.0
=============================
.. _CVE-2025-53865L:
XSS security issue with devel and responsive templates (recommended)
--------------------------------------------------------------------
There are actually two different issues under this heading.
1. incorrect use of the ``structure`` keyword with
``tal:content``
2. use of ``tal:replace`` on unsafe input
See the `security page for a link to CVE-2025-53865
`_.
In the discussion below, the :term:`html directory` means one or
more directories listed in the ``templates`` key of your
tracker's ``config.ini`` file.
These directions can be used to solve the XSS security issue with
any version of Roundup. Even if you used a classic or minimal
template, you should check your trackers for these issues. The
classic template fixed most of these many years ago, but the
updates were not made to the devel and responsive templates. No
report of similar issues with the jinja template has been seen.
Incorrect use of structure in templates
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The devel and responsive templates prior to Roundup 2.5 used this
construct::
tal:content="structure context/MUMBLE/plain"
Where ``MUMBLE`` is a property of your issues (e.g. title).
This construct allows a URL with a carefully crafted query
parameter to execute arbitrary JavaScript.
You should check all your trackers. The classic template has not
used this construct since at least 2009, but your tracker's
templates may use the offending construct anyway.
This fix will apply if your tracker is based on the responsive or
devel template. Check the TEMPLATE-INFO.txt file in your tracker
home. The template name is the first component of the ``Name``
field. For example a Name like::
Name: responsive-bugtracker
Name: devel-bugtracker
shows that tracker is based on the responsive or devel templates.
.. _cve-2025-53865-fixed:
To fix this, remove the ``structure`` declaration when it is used
with a plain representation. So fixing the code by replacing the
example above with::
tal:content="context/MUMBLE/plain"
prevents the attack.
To check for this issue, search for ``structure`` followed by
``/plain`` in all your html templates. If you are on a Linux/Unix
system you can search the html subdirectory of your tracker with
the following::
grep 'structure.*/plain' *.html
which should return any lines with issues.
.. warning::
Backup the files in the ``html`` subdirectory of your tracker
in case an edit goes wrong.
As an example, you could fix this issue using the GNU sed
command::
sed -i.bak -e '/structure.*\/plain/s/structure.//' *.html
to edit the files in place and remove the structure keyword. It
will create a ``.bak`` file with the original contents of the
file. If your templates were changed, this might still miss some
entries. If you are on windows, some text editors support search
and replace using a regular expression.
If the construct is split across lines::
tal:content="structure
context/MUMBLE/plain"
the commands above will miss the construct. So you should also
search the html files using ``grep /plain *.html`` and verify
that all of the ``context/MUMBLE/plain`` include ``tal:content``
as in the `fixed example above <#cve-2025-53865-fixed>`_. Any
lines that have ``context/MUMBLE/plain`` without ``tal:content=``
before it need to be manually verified/fixed.
The distributed devel and responsive templates do not split the
construct across lines, but if you changed the files it may be
split.
tal:replace used with unsafe input
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The problem was caused by the following markup::
in the head of the ``bug.item.html``, ``task.item.html`` and
other files in the devel and responsive templates.
This was fixed many years ago in the classic template's
``index.item.html``. The classic template replaces the above
construct with::
``tal:content`` explicitly escapes the result unless the
``structure`` directive is used. ``tal:replace`` expects the
result to be safe and usable in an HTML context.
TAL drops any tags that it doesn't know about from the output.
```` results in the value of the
content expression without a surrounding html tag. (Effectively
replacing the construct.)
The following diff for ``bug.item.html`` in the devel template
shows the change to make things safe (remove lines starting with
``-`` and add lines staring with ``+``)::
Bug : - Bug : - New Bug report - username
to::
username
The code audit found the ``tal:replace`` construct is used with
``context/id`` and ``context/designator`` paths. The references
to these paths have been changed to use ``tal:x`` in the classic
template's ``msg.item.html`` file and the classic and minimal
template's ``_generic.collision.html`` file.
These paths are critical to navigation in Roundup and are set
from the path part of the URL. Roundup's URL path validation
makes it unlikely that an attacker could exploit them. If you
wish you can change your templates or copy the corresponding
files from the template if you haven't made local changes.
Also you may have used copies of these insecure templates
elsewhere in your tracker (e.g. to create a feature class). To
find other possible issues you can use the command::
grep -r "tal:replace=" *.html
in your tracker's :term:`html directory`. Check each occurrence
and if needed, change it to the safer form. You should consider
any reference to ``context`` to be under the user's (attacker's)
control. Also ``db`` (excluding ``db/config``) and ``request``
references that use user supplied content
(e.g. ``request/user/username`` above) should be changed to
``tal:x`` form
.. comment:
As part of the analysis, the following command was used to find
potentially vulnerable stuff in the templates. Each grep -v was
removed to display items in that category and they were checked::
grep -r 'tal:replace' . | grep -v 'replace="batch' | \
grep -v 'replace="config' | grep -v 'replace="db/config' | \
grep -v 'replace="structure' | grep -v 'replace="python:' | \
grep -v 'replace="request/'
context/id, context/designator:
assume safe if used in an class.item.html page as the page
wouldn't be shown if they weren't valid numbers/designators.
Might not be ok referenced in a _generic fallback page though.
config, db/config, batch, nothing:
should be safe as they are not under user control
request/classname (python:request._classname), request/template:
should be safe as they are needed to navigate to a display page,
so if they are invalid nothing will be displayed.
utils, python:
assume it's written correctly and is safe (could use some new
tests for the shipped utility functions). The intent of these
can be to deliver blocks of
or get the file in a separate download using a regular script
tag::
You can place these at the end of ``page.html`` just before the
close body ``