CVE-2019-25101: Interpretation Conflict (CWE-436)
value: A vulnerability classified as critical has been found in OnShift TurboGears 1.0.11.10. This affects an unknown part of the file turbogears/controllers.py of the component HTTP Header Handler. The manipulation leads to http response splitting. It is possible to initiate the attack remotely. Upgrading to version 1.0.11.11 is able to address this issue. The patch is named f68bbaba47f4474e1da553aa51564a73e1d92a84. It is recommended to upgrade the affected component. The associated identifier of this vulnerability is VDB-220059.
CWE description: Product A handles inputs or steps differently than Product B, which causes A to perform incorrect actions based on its perception of B's state.
CVSS v3 base score: 9.8
Published: 2023-02-04T08:15Z
Repository: https://github.com/OnShift/turbogears
Language: Python
Files changed: turbogears/controllers.py, turbogears/release.py, turbogears/tests/test_controllers.py
Commit date: 2019-01-31 09:18:19 -0500
Commit message: SWAT-20: Remove header injection vulnerability on redirect

There was a Header Injection issue (remember HTTP is a text protocol and new
lines matter).  Basically you could inject a header or even the text with
certain parameters.  Typically, this failed on redirects because of the location
header.  We now throw an exception if the redirect url includes a new line or
carriage return.  Neither of these are valid (though you can presumably escape
them).

Diff: diff --git a/turbogears/controllers.py b/turbogears/controllers.py
index d6ad92f..1a7b651 100644
--- a/turbogears/controllers.py
+++ b/turbogears/controllers.py
@@ -323,11 +323,11 @@ def expose(template=None, validators=None, allow_json=None, html=None,
     @keyparam html deprecated in favor of template
     @keyparam validators deprecated. Maps argument names to validator
             applied to that arg
     @keyparam inputform deprecated. A form object that generates the
             input to this method
-    @keyparam exclude_from_memory_profiling allows to exclude individual end points from memory profiling. Can be 
+    @keyparam exclude_from_memory_profiling allows to exclude individual end points from memory profiling. Can be
             used for performance or in case profiling generates errors
     """
     if html:
         template = html
     if not template:
@@ -600,64 +600,71 @@ def check_app_root():
     if app_root.endswith('/'):
         app_root = app_root[:-1]
     request.app_root = app_root
 
 
-def get_server_name(): 
-    """Return name of the server this application runs on. 
- 
-    Respects 'Host' and 'X-Forwarded-Host' header. 
- 
-    See the docstring of the 'absolute_url' function for more information. 
- 
-    """ 
-    get = config.get 
-    h = request.headers 
-    host = get('tg.url_domain') or h.get('X-Forwarded-Host', h.get('Host')) 
-    if not host: 
-        host = '%s:%s' % (get('server.socket_host', 'localhost'), 
-            get('server.socket_port', 8080)) 
-    return host 
-
-
-def absolute_url(tgpath='/', params=None, **kw): 
-    """Return absolute URL (including schema and host to this server). 
- 
-    Tries to account for 'Host' header and reverse proxying 
-    ('X-Forwarded-Host'). 
- 
-    The host name is determined this way: 
- 
-    * If the config setting 'tg.url_domain' is set and non-null, use this value. 
-    * Else, if the 'base_url_filter.use_x_forwarded_host' config setting is 
-      True, use the value from the 'Host' or 'X-Forwarded-Host' request header. 
-    * Else, if config setting 'base_url_filter.on' is True and 
-      'base_url_filter.base_url' is non-null, use its value for the host AND 
-      scheme part of the URL. 
-    * As a last fallback, use the value of 'server.socket_host' and 
-      'server.socket_port' config settings (defaults to 'localhost:8080'). 
- 
-    The URL scheme ('http' or 'http') used is determined in the following way: 
- 
-    * If 'base_url_filter.base_url' is used, use the scheme from this URL. 
-    * If there is a 'X-Use-SSL' request header, use 'https'. 
-    * Else, if the config setting 'tg.url_scheme' is set, use its value. 
-    * Else, use the value of 'cherrypy.request.scheme'. 
- 
-    """ 
-    get = config.get 
-    use_xfh = get('base_url_filter.use_x_forwarded_host', False) 
-    if request.headers.get('X-Use-SSL'): 
-        scheme = 'https' 
-    else: 
-        scheme = get('tg.url_scheme') 
-    if not scheme: 
-        scheme = request.scheme 
-    base_url = '%s://%s' % (scheme, get_server_name()) 
-    if get('base_url_filter.on', False) and not use_xfh: 
-        base_url = get('base_url_filter.base_url').rstrip('/') 
-    return '%s%s' % (base_url, url(tgpath, params, **kw)) 
+def get_server_name():
+    """Return name of the server this application runs on.
+
+    Respects 'Host' and 'X-Forwarded-Host' header.
+
+    See the docstring of the 'absolute_url' function for more information.
+
+    """
+    get = config.get
+    h = request.headers
+    host = get('tg.url_domain') or h.get('X-Forwarded-Host', h.get('Host'))
+    if not host:
+        host = '%s:%s' % (get('server.socket_host', 'localhost'),
+            get('server.socket_port', 8080))
+    return host
+
+
+def absolute_url(tgpath='/', params=None, **kw):
+    """Return absolute URL (including schema and host to this server).
+
+    Tries to account for 'Host' header and reverse proxying
+    ('X-Forwarded-Host').
+
+    The host name is determined this way:
+
+    * If the config setting 'tg.url_domain' is set and non-null, use this value.
+    * Else, if the 'base_url_filter.use_x_forwarded_host' config setting is
+      True, use the value from the 'Host' or 'X-Forwarded-Host' request header.
+    * Else, if config setting 'base_url_filter.on' is True and
+      'base_url_filter.base_url' is non-null, use its value for the host AND
+      scheme part of the URL.
+    * As a last fallback, use the value of 'server.socket_host' and
+      'server.socket_port' config settings (defaults to 'localhost:8080').
+
+    The URL scheme ('http' or 'http') used is determined in the following way:
+
+    * If 'base_url_filter.base_url' is used, use the scheme from this URL.
+    * If there is a 'X-Use-SSL' request header, use 'https'.
+    * Else, if the config setting 'tg.url_scheme' is set, use its value.
+    * Else, use the value of 'cherrypy.request.scheme'.
+
+    """
+    get = config.get
+    use_xfh = get('base_url_filter.use_x_forwarded_host', False)
+    if request.headers.get('X-Use-SSL'):
+        scheme = 'https'
+    else:
+        scheme = get('tg.url_scheme')
+    if not scheme:
+        scheme = request.scheme
+    base_url = '%s://%s' % (scheme, get_server_name())
+    if get('base_url_filter.on', False) and not use_xfh:
+        base_url = get('base_url_filter.base_url').rstrip('/')
+    return '%s%s' % (base_url, url(tgpath, params, **kw))
+
+
+class InvalidRedirectException(Exception):
+    """
+    An invalid redirect url was provided.  Redirects cannot
+    include a carriage return (\r) or new line (\n) character.
+    """
 
 
 def redirect(redirect_path, redirect_params=None, **kw):
     """Redirect (via cherrypy.HTTPRedirect).
 
@@ -671,10 +678,12 @@ def redirect(redirect_path, redirect_params=None, **kw):
         path = request.path_info
         check_app_root()
         if path.startswith(request.app_root):
             path = path[len(request.app_root):]
         redirect_path = urlparse.urljoin(path, redirect_path)
+    if set(redirect_path).intersection({'\r', '\n'}):
+        raise InvalidRedirectException('Invalid redirect: {}'.format(redirect_path))
     raise cherrypy.HTTPRedirect(url(tgpath=redirect_path,
         tgparams=redirect_params, **kw))
 
 
 __all__ = [
diff --git a/turbogears/release.py b/turbogears/release.py
index 91384ae..f8c99ae 100644
--- a/turbogears/release.py
+++ b/turbogears/release.py
@@ -26,11 +26,11 @@ problem.
 
 .. _TurboGears subversion repository:
     http://svn.turbogears.org/trunk#egg=turbogears-dev
 """
 
-version = "1.0.11.8"
+version = "1.0.11.9"
 description = "Front-to-back, open-source, rapid web development framework"
 long_description = __doc__
 author = "Kevin Dangoor"
 email = "dangoor+turbogears@gmail.com"
 maintainer = "TurboGears Release Team"
diff --git a/turbogears/tests/test_controllers.py b/turbogears/tests/test_controllers.py
index 5b4c11c..1bac258 100644
--- a/turbogears/tests/test_controllers.py
+++ b/turbogears/tests/test_controllers.py
@@ -255,10 +255,14 @@ class MyRoot(controllers.RootController):
 
     [expose()]
     def redirect_to_path_tuple(self, path):
         raise redirect((path, 'index'))
 
+    [expose()]
+    def redirect_error(self):
+        raise redirect('/blah\r\n')
+
 
 class TestRoot(unittest.TestCase):
 
     def setUp(self):
         cherrypy.root = None
@@ -710,10 +714,15 @@ class TestURLs(unittest.TestCase):
                 testutil.create_request(url)
                 assert cherrypy.response.status.startswith("302"), url
                 assert (cherrypy.response.headers['Location']
                     == 'http://localhost/subthing/index'), url
 
+    def test_redirect_error(self):
+        url = "/redirect_error"
+        testutil.create_request(url)
+        assert cherrypy.response.status.startswith("500")
+
     def test_multi_values(self):
         testutil.create_request("/")
         assert url("/foo", bar=[1, 2]) in \
             ["/foo?bar=1&bar=2", "/foo?bar=2&bar=1"]
         assert url("/foo", bar=("asdf", "qwer")) in \

Vulnerable code:     @keyparam exclude_from_memory_profiling allows to exclude individual end points from memory profiling. Can be 
def get_server_name(): 
    """Return name of the server this application runs on. 
 
    Respects 'Host' and 'X-Forwarded-Host' header. 
 
    See the docstring of the 'absolute_url' function for more information. 
 
    """ 
    get = config.get 
    h = request.headers 
    host = get('tg.url_domain') or h.get('X-Forwarded-Host', h.get('Host')) 
    if not host: 
        host = '%s:%s' % (get('server.socket_host', 'localhost'), 
            get('server.socket_port', 8080)) 
    return host 


def absolute_url(tgpath='/', params=None, **kw): 
    """Return absolute URL (including schema and host to this server). 
 
    Tries to account for 'Host' header and reverse proxying 
    ('X-Forwarded-Host'). 
 
    The host name is determined this way: 
 
    * If the config setting 'tg.url_domain' is set and non-null, use this value. 
    * Else, if the 'base_url_filter.use_x_forwarded_host' config setting is 
      True, use the value from the 'Host' or 'X-Forwarded-Host' request header. 
    * Else, if config setting 'base_url_filter.on' is True and 
      'base_url_filter.base_url' is non-null, use its value for the host AND 
      scheme part of the URL. 
    * As a last fallback, use the value of 'server.socket_host' and 
      'server.socket_port' config settings (defaults to 'localhost:8080'). 
 
    The URL scheme ('http' or 'http') used is determined in the following way: 
 
    * If 'base_url_filter.base_url' is used, use the scheme from this URL. 
    * If there is a 'X-Use-SSL' request header, use 'https'. 
    * Else, if the config setting 'tg.url_scheme' is set, use its value. 
    * Else, use the value of 'cherrypy.request.scheme'. 
 
    """ 
    get = config.get 
    use_xfh = get('base_url_filter.use_x_forwarded_host', False) 
    if request.headers.get('X-Use-SSL'): 
        scheme = 'https' 
    else: 
        scheme = get('tg.url_scheme') 
    if not scheme: 
        scheme = request.scheme 
    base_url = '%s://%s' % (scheme, get_server_name()) 
    if get('base_url_filter.on', False) and not use_xfh: 
        base_url = get('base_url_filter.base_url').rstrip('/') 
    return '%s%s' % (base_url, url(tgpath, params, **kw)) 
version = "1.0.11.8"
Fixed code:     @keyparam exclude_from_memory_profiling allows to exclude individual end points from memory profiling. Can be
def get_server_name():
    """Return name of the server this application runs on.

    Respects 'Host' and 'X-Forwarded-Host' header.

    See the docstring of the 'absolute_url' function for more information.

    """
    get = config.get
    h = request.headers
    host = get('tg.url_domain') or h.get('X-Forwarded-Host', h.get('Host'))
    if not host:
        host = '%s:%s' % (get('server.socket_host', 'localhost'),
            get('server.socket_port', 8080))
    return host


def absolute_url(tgpath='/', params=None, **kw):
    """Return absolute URL (including schema and host to this server).

    Tries to account for 'Host' header and reverse proxying
    ('X-Forwarded-Host').

    The host name is determined this way:

    * If the config setting 'tg.url_domain' is set and non-null, use this value.
    * Else, if the 'base_url_filter.use_x_forwarded_host' config setting is
      True, use the value from the 'Host' or 'X-Forwarded-Host' request header.
    * Else, if config setting 'base_url_filter.on' is True and
      'base_url_filter.base_url' is non-null, use its value for the host AND
      scheme part of the URL.
    * As a last fallback, use the value of 'server.socket_host' and
      'server.socket_port' config settings (defaults to 'localhost:8080').

    The URL scheme ('http' or 'http') used is determined in the following way:

    * If 'base_url_filter.base_url' is used, use the scheme from this URL.
    * If there is a 'X-Use-SSL' request header, use 'https'.
    * Else, if the config setting 'tg.url_scheme' is set, use its value.
    * Else, use the value of 'cherrypy.request.scheme'.

    """
    get = config.get
    use_xfh = get('base_url_filter.use_x_forwarded_host', False)
    if request.headers.get('X-Use-SSL'):
        scheme = 'https'
    else:
        scheme = get('tg.url_scheme')
    if not scheme:
        scheme = request.scheme
    base_url = '%s://%s' % (scheme, get_server_name())
    if get('base_url_filter.on', False) and not use_xfh:
        base_url = get('base_url_filter.base_url').rstrip('/')
    return '%s%s' % (base_url, url(tgpath, params, **kw))


class InvalidRedirectException(Exception):
    """
    An invalid redirect url was provided.  Redirects cannot
    include a carriage return (\r) or new line (\n) character.
    """
    if set(redirect_path).intersection({'\r', '\n'}):
        raise InvalidRedirectException('Invalid redirect: {}'.format(redirect_path))
version = "1.0.11.9"
    [expose()]
    def redirect_error(self):
        raise redirect('/blah\r\n')

    def test_redirect_error(self):
        url = "/redirect_error"
        testutil.create_request(url)
        assert cherrypy.response.status.startswith("500")