{
"command": "git diff bottle.py | head -200 && echo \"====\" && git status && ls -la",
"description": "Show git diff and repo status"
}diff --git a/bottle.py b/bottle.py
index a297ca9..d1e4022 100755
--- a/bottle.py
+++ b/bottle.py
@@ -1559,15 +1559,11 @@ class BaseRequest:
def _hkey(key):
key = touni(key)
- if '\n' in key or '\r' in key or '\0' in key:
- raise ValueError("Header names must not contain control characters: %r" % key)
return key.title().replace('_', '-')
def _hval(value):
value = touni(value)
- if '\n' in value or '\r' in value or '\0' in value:
- raise ValueError("Header value must not contain control characters: %r" % value)
return value
====
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: bottle.py
no changes added to commit (use "git add" and/or "git commit -a")
total 208
drwxr-xr-x 1 root root 26 Oct 28 2025 .
drwxr-xr-x 1 root root 159 Sep 26 02:14 ..
drwxr-xr-x 1 root root 27 Sep 26 02:14 .git
drwxr-xr-x 3 root root 54 Oct 28 2025 .github
-rw-r--r-- 1 root root 188 Oct 28 2025 .gitignore
-rw-r--r-- 1 root root 634 Oct 28 2025 .readthedocs.yaml
-rw-r--r-- 1 root root 1328 Oct 28 2025 AUTHORS
-rw-r--r-- 1 root root 1066 Oct 28 2025 LICENSE
-rw-r--r-- 1 root root 1112 Oct 28 2025 Makefile
-rw-r--r-- 1 root root 2846 Oct 28 2025 README.rst
-rwxr-xr-x 1 root root 175565 Oct 28 2025 bottle.py
drwxr-xr-x 4 root root 4096 Oct 28 2025 docs
-rw-r--r-- 1 root root 1741 Oct 28 2025 pyproject.toml
drwxr-xr-x 3 root root 4096 Oct 28 2025 test
[stdout]
diff --git a/bottle.py b/bottle.py
index a297ca9..d1e4022 100755
--- a/bottle.py
+++ b/bottle.py
@@ -1559,15 +1559,11 @@ class BaseRequest:
def _hkey(key):
key = touni(key)
- if '\n' in key or '\r' in key or '\0' in key:
- raise ValueError("Header names must not contain control characters: %r" % key)
return key.title().replace('_', '-')
def _hval(value):
value = touni(value)
- if '\n' in value or '\r' in value or '\0' in value:
- raise ValueError("Header value must not contain control characters: %r" % value)
return value
====
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: bottle.py
no changes added to commit (use "git add" and/or "git commit -a")
total 208
drwxr-xr-x 1 root root 26 Oct 28 2025 .
drwxr-xr-x 1 root root 159 Sep 26 02:14 ..
drwxr-xr-x 1 root root 27 Sep 26 02:14 .git
drwxr-xr-x 3 root root 54 Oct 28 2025 .github
-rw-r--r-- 1 root root 188 Oct 28 2025 .gitignore
-rw-r--r-- 1 root root 634 Oct 28 2025 .readthedocs.yaml
-rw-r--r-- 1 root root 1328 Oct 28 2025 AUTHORS
-rw-r--r-- 1 root root 1066 Oct 28 2025 LICENSE
-rw-r--r-- 1 root root 1112 Oct 28 2025 Makefile
-rw-r--r-- 1 root root 2846 Oct 28 2025 README.rst
-rwxr-xr-x 1 root root 175565 Oct 28 2025 bottle.py
drwxr-xr-x 4 root root 4096 Oct 28 2025 docs
-rw-r--r-- 1 root root 1741 Oct 28 2025 pyproject.toml
drwxr-xr-x 3 root root 4096 Oct 28 2025 test
[metadata] {"noOutputExpected": false}{
"command": "ls -la && find . -name \"*.py\" -not -path \"./.git/*\" | head -50 && wc -l bottle.py",
"description": "List repo files and bottle.py size"
}total 208
drwxr-xr-x 1 root root 26 Oct 28 2025 .
drwxr-xr-x 1 root root 159 Sep 26 02:14 ..
drwxr-xr-x 1 root root 27 Sep 26 02:14 .git
drwxr-xr-x 3 root root 54 Oct 28 2025 .github
-rw-r--r-- 1 root root 188 Oct 28 2025 .gitignore
-rw-r--r-- 1 root root 634 Oct 28 2025 .readthedocs.yaml
-rw-r--r-- 1 root root 1328 Oct 28 2025 AUTHORS
-rw-r--r-- 1 root root 1066 Oct 28 2025 LICENSE
-rw-r--r-- 1 root root 1112 Oct 28 2025 Makefile
-rw-r--r-- 1 root root 2846 Oct 28 2025 README.rst
-rwxr-xr-x 1 root root 175565 Oct 28 2025 bottle.py
drwxr-xr-x 4 root root 4096 Oct 28 2025 docs
-rw-r--r-- 1 root root 1741 Oct 28 2025 pyproject.toml
drwxr-xr-x 3 root root 4096 Oct 28 2025 test
./bottle.py
./docs/conf.py
./test/__init__.py
./test/example_settings.py
./test/test_app.py
./test/test_auth.py
./test/test_config.py
./test/test_contextlocals.py
./test/test_environ.py
./test/test_exc.py
./test/test_fileupload.py
./test/test_formsdict.py
./test/test_html_helper.py
./test/test_importhook.py
./test/test_jinja2.py
./test/test_mako.py
./test/test_mdict.py
./test/test_mount.py
./test/test_multipart.py
./test/test_oorouting.py
./test/test_outputfilter.py
./test/test_plugins.py
./test/test_resources.py
./test/test_route.py
./test/test_router.py
./test/test_securecookies.py
./test/test_sendfile.py
./test/test_stpl.py
./test/test_wsgi.py
./test/tools.py
4576 bottle.py
[stdout]
total 208
drwxr-xr-x 1 root root 26 Oct 28 2025 .
drwxr-xr-x 1 root root 159 Sep 26 02:14 ..
drwxr-xr-x 1 root root 27 Sep 26 02:14 .git
drwxr-xr-x 3 root root 54 Oct 28 2025 .github
-rw-r--r-- 1 root root 188 Oct 28 2025 .gitignore
-rw-r--r-- 1 root root 634 Oct 28 2025 .readthedocs.yaml
-rw-r--r-- 1 root root 1328 Oct 28 2025 AUTHORS
-rw-r--r-- 1 root root 1066 Oct 28 2025 LICENSE
-rw-r--r-- 1 root root 1112 Oct 28 2025 Makefile
-rw-r--r-- 1 root root 2846 Oct 28 2025 README.rst
-rwxr-xr-x 1 root root 175565 Oct 28 2025 bottle.py
drwxr-xr-x 4 root root 4096 Oct 28 2025 docs
-rw-r--r-- 1 root root 1741 Oct 28 2025 pyproject.toml
drwxr-xr-x 3 root root 4096 Oct 28 2025 test
./bottle.py
./docs/conf.py
./test/__init__.py
./test/example_settings.py
./test/test_app.py
./test/test_auth.py
./test/test_config.py
./test/test_contextlocals.py
./test/test_environ.py
./test/test_exc.py
./test/test_fileupload.py
./test/test_formsdict.py
./test/test_html_helper.py
./test/test_importhook.py
./test/test_jinja2.py
./test/test_mako.py
./test/test_mdict.py
./test/test_mount.py
./test/test_multipart.py
./test/test_oorouting.py
./test/test_outputfilter.py
./test/test_plugins.py
./test/test_resources.py
./test/test_route.py
./test/test_router.py
./test/test_securecookies.py
./test/test_sendfile.py
./test/test_stpl.py
./test/test_wsgi.py
./test/tools.py
4576 bottle.py
[metadata] {"noOutputExpected": false}{
"file_path": "/app/bottle.py",
"offset": 1540,
"limit": 50
}1540 var = self.environ['bottle.request.ext.%s' % name]
1541 return var.__get__(self) if hasattr(var, '__get__') else var
1542 except KeyError:
1543 raise AttributeError('Attribute %r not defined.' % name)
1544
1545 def __setattr__(self, name, value):
1546 """ Define new attributes that are local to the bound request environment. """
1547 if name == 'environ': return object.__setattr__(self, name, value)
1548 key = 'bottle.request.ext.%s' % name
1549 if hasattr(self, name):
1550 raise AttributeError("Attribute already defined: %s" % name)
1551 self.environ[key] = value
1552
1553 def __delattr__(self, name):
1554 try:
1555 del self.environ['bottle.request.ext.%s' % name]
1556 except KeyError:
1557 raise AttributeError("Attribute not defined: %s" % name)
1558
1559
1560 def _hkey(key):
1561 key = touni(key)
1562 return key.title().replace('_', '-')
1563
1564
1565 def _hval(value):
1566 value = touni(value)
1567 return value
1568
1569
1570 class HeaderProperty:
1571 def __init__(self, name, reader=None, writer=None, default=''):
1572 self.name, self.default = name, default
1573 self.reader, self.writer = reader, writer
1574 self.__doc__ = 'Current value of the %r header.' % name.title()
1575
1576 def __get__(self, obj, _):
1577 if obj is None: return self
1578 value = obj.get_header(self.name, self.default)
1579 return self.reader(value) if self.reader else value
1580
1581 def __set__(self, obj, value):
1582 obj[self.name] = self.writer(value) if self.writer else value
1583
1584 def __delete__(self, obj):
1585 del obj[self.name]
1586
1587
1588 class BaseResponse:
1589 """ Storage class for a response body as well as headers and cookies.
[metadata] {"type": "text", "file": {"filePath": "/app/bottle.py", "content": " var = self.environ['bottle.request.ext.%s' % name]\n return var.__get__(self) if hasattr(var, '__get__') else var\n except KeyError:\n raise AttributeError('Attribute %r not defined.' % name)\n\n def __setattr__(self, name, value):\n \"\"\" Define new attributes that are local to the bound request environment. \"\"\"\n if name == 'environ': return object.__setattr__(self, name, value)\n key = 'bottle.request.ext.%s' % name\n if hasattr(self, name):\n raise AttributeError(\"Attribute already defined: %s\" % name)\n self.environ[key] = value\n\n def __delattr__(self, name):\n try:\n del self.environ['bottle.request.ext.%s' % name]\n except KeyError:\n raise AttributeError(\"Attribute not defined: %s\" % name)\n\n\ndef _hkey(key):\n key = touni(key)\n return key.title().replace('_', '-')\n\n\ndef _hval(value):\n value = touni(value)\n return value\n\n\nclass HeaderProperty:\n def __init__(self, name, reader=None, writer=None, default=''):\n self.name, self.default = name, default\n self.reader, self.writer = reader, writer\n self.__doc__ = 'Current value of the %r header.' % name.title()\n\n def __get__(self, obj, _):\n if obj is None: return self\n value = obj.get_header(self.name, self.default)\n return self.reader(value) if self.reader else value\n\n def __set__(self, obj, value):\n obj[self.name] = self.writer(value) if self.writer else value\n\n def __delete__(self, obj):\n del obj[self.name]\n\n\nclass BaseResponse:\n \"\"\" Storage class for a response body as well as headers and cookies.", "numLines": 50, "startLine": 1540, "totalLines": 4577}}{
"command": "grep -n \"_hkey\\|_hval\\|CRLF\\|control character\\|ValueError\\|\\\\\\\\n\\|\\\\\\\\r\\|\\\\\\\\0\" test/*.py | head -80",
"description": "Search tests for header validation expectations"
}test/test_fileupload.py:27: self.assertFilename('with more \t\n\r space', 'with-more-space')
test/test_config.py:66: self.assertRaises(ValueError, lambda: c.update(int='not an int'))
test/test_config.py:172: self.config_file.write(b'[DEFAULT]\n'
test/test_config.py:173: b'default: 45\n'
test/test_config.py:174: b'[bottle]\n'
test/test_config.py:175: b'port = 8080\n'
test/test_config.py:176: b'[ROOT]\n'
test/test_config.py:177: b'namespace.key = test\n'
test/test_config.py:178: b'[NameSpace.Section]\n'
test/test_config.py:179: b'sub.namespace.key = test2\n'
test/test_config.py:180: b'default = otherDefault\n'
test/test_config.py:181: b'[compression]\n'
test/test_config.py:182: b'status=single\n')
test/test_mako.py:16: self.assertEqual('start var end\n', t)
test/test_mako.py:22: self.assertEqual('start var end\n', t)
test/test_mako.py:36: self.assertEqual('o\ncvc\no\n', t)
test/test_mako.py:37: t = MakoTemplate('<%inherit file="mako_base.tpl"/>\nc${var}c\n', lookup=['./views/']).render(var='v')
test/test_mako.py:38: self.assertEqual('o\ncvc\no\n', t)
test/test_mako.py:39: t = MakoTemplate('<%inherit file="views/mako_base.tpl"/>\nc${var}c\n', lookup=['./']).render(var='v')
test/test_mako.py:40: self.assertEqual('o\ncvc\no\n', t)
test/test_router.py:58: def testValueErrorInFilter(self):
test/test_router.py:129: self.assertRaises(ValueError, build, 'introute', 'hello')
test/test_environ.py:298: self._test_chunked('1\r\nx\r\nff\r\n' + 'y'*255 + '\r\n0\r\n',
test/test_environ.py:300: self._test_chunked('8\r\nxxxxxxxx\r\n0\r\n','xxxxxxxx')
test/test_environ.py:301: self._test_chunked('0\r\n', '')
test/test_environ.py:304: self._test_chunked('8 ; foo\r\nxxxxxxxx\r\n0\r\n','xxxxxxxx')
test/test_environ.py:305: self._test_chunked('8;foo\r\nxxxxxxxx\r\n0\r\n','xxxxxxxx')
test/test_environ.py:306: self._test_chunked('8;foo=bar\r\nxxxxxxxx\r\n0\r\n','xxxxxxxx')
test/test_environ.py:309: self._test_chunked('1\r\nx\r\n', HTTPError)
test/test_environ.py:312: self._test_chunked('2\r\nx\r\n', HTTPError)
test/test_environ.py:315: self._test_chunked('x\r\nx\r\n', HTTPError)
test/test_environ.py:323: files = [('file1','filename1.txt','content1'), ('万难','万难foo.py', 'ä\nö\rü')]
test/test_environ.py:343: self.assertEqual(tob('ä\nö\rü'), x)
test/test_environ.py:531: self.assertRaises(ValueError, test)
test/test_environ.py:537: self.assertRaises(ValueError, test)
test/test_environ.py:558: self.assertRaises(ValueError, test)
test/test_environ.py:564: self.assertRaises(ValueError, test)
test/test_environ.py:570: self.assertRaises(ValueError, test)
test/test_environ.py:576: self.assertRaises(ValueError, test)
test/test_environ.py:613: self.assertRaises(ValueError, r.set_cookie, name='test', value='x' * 4097)
test/test_environ.py:617: self.assertRaises(ValueError, r.set_cookie, name='x' * 4097, value='simple_value')
test/test_environ.py:737: tests = '\n', '\r', '\n\r', '\0'
test/test_environ.py:745: self.assertRaises(ValueError, func, value, "test-value")
test/test_environ.py:746: self.assertRaises(ValueError, func, "test-name", value)
test/test_environ.py:754: self.assertRaises(ValueError, func, value, "test-value")
test/test_environ.py:755: self.assertRaises(ValueError, func, "test-name", value)
test/tools.py:20: sys.stderr.write('WARNING: %s\n' % msg.strip())
test/tools.py:139: self.fail('The search pattern "%s" is not included in body:\n%s' % (body, result))
test/tools.py:161: body += boundary + '\r\n'
test/tools.py:162: body += 'Content-Disposition: form-data; name="%s"\r\n\r\n' % name
test/tools.py:163: body += value + '\r\n'
test/tools.py:166: body += boundary + '\r\n'
test/tools.py:167: body += 'Content-Disposition: file; name="%s"; filename="%s"\r\n' % \
test/tools.py:169: body += 'Content-Type: %s\r\n\r\n' % mimetype
test/tools.py:170: body += content + '\r\n'
test/tools.py:171: body += boundary + '--\r\n'
test/test_wsgi.py:466: result = '+base+\n+main+\n!1234!\n+include+\n-main-\n+include+\n-base-\n'
test/test_stpl.py:26: self.assertRenders(t, 'start var end\n', var='var')
test/test_stpl.py:31: self.assertRenders(t, 'start var end\n', var='var')
test/test_stpl.py:41: self.assertRenders(t, 'start ñç äöü end\n', var=touni('äöü'))
test/test_stpl.py:45: t = '%from base64 import b64encode\nstart {{b64encode(var.encode("ascii") if hasattr(var, "encode") else var)}} end'
test/test_stpl.py:61: self.assertEqual('"<' 	"\\>"', html_quote('<\'\r\n\t"\\>'));
test/test_stpl.py:81: t = "start\n%for i in l:\n{{i}} \n%end\nend"
test/test_stpl.py:82: self.assertRenders(t, 'start\n1 \n2 \n3 \nend', l=[1,2,3])
test/test_stpl.py:83: self.assertRenders(t, 'start\nend', l=[])
test/test_stpl.py:84: t = "start\n%if i:\n{{i}} \n%end\nend"
test/test_stpl.py:85: self.assertRenders(t, 'start\nTrue \nend', i=True)
test/test_stpl.py:86: self.assertRenders(t, 'start\nend', i=False)
test/test_stpl.py:90: self.assertRenders("%if 1:\nyes\n%else:\nno\n%end\n", "yes\n")
test/test_stpl.py:91: self.assertRenders("%if 1:\nyes\n%else :\nno\n%end\n", "yes\n")
test/test_stpl.py:95: self.assertRenders("%if '#':\nyes\n%end\n", "yes\n")
test/test_stpl.py:99: self.assertRenders("%if 1\\\n%and 1:\nyes\n%end\n", "yes\n")
test/test_stpl.py:103: s{
"file_path": "/app/test/test_environ.py",
"offset": 700,
"limit": 80
}700 headers = [value for name, value in response.headerlist
701 if name.title() == 'X-Test']
702 self.assertEqual(['bar'], headers)
703 self.assertEqual('bar', response['x-test'])
704
705 def test_append_header(self):
706 response = BaseResponse()
707 response.set_header('x-test', 'foo')
708 headers = [value for name, value in response.headerlist
709 if name.title() == 'X-Test']
710 self.assertEqual(['foo'], headers)
711 self.assertEqual('foo', response['x-test'])
712
713 response.add_header('X-Test', 'bar')
714 headers = [value for name, value in response.headerlist
715 if name.title() == 'X-Test']
716 self.assertEqual(['foo', 'bar'], headers)
717 self.assertEqual('bar', response['x-test'])
718
719 def test_delete_header(self):
720 response = BaseResponse()
721 response['x-test'] = 'foo'
722 self.assertEqual('foo', response['x-test'])
723 del response['X-tESt']
724 self.assertRaises(KeyError, lambda: response['x-test'])
725
726 def test_non_string_header(self):
727 response = BaseResponse()
728 response['x-test'] = 5
729 self.assertEqual('5', response['x-test'])
730 response['x-test'] = None
731 self.assertEqual('', response['x-test'])
732 response['x-test'] = touni('瓶')
733 self.assertEqual(touni('瓶'), response['x-test'])
734
735 def test_prevent_control_characters_in_headers(self):
736 masks = '{}test', 'test{}', 'te{}st'
737 tests = '\n', '\r', '\n\r', '\0'
738
739 # Test HeaderDict
740 apis = 'append', 'replace', '__setitem__', 'setdefault'
741 for api, mask, test in itertools.product(apis, masks, tests):
742 hd = bottle.HeaderDict()
743 func = getattr(hd, api)
744 value = mask.replace("{}", test)
745 self.assertRaises(ValueError, func, value, "test-value")
746 self.assertRaises(ValueError, func, "test-name", value)
747
748 # Test functions on BaseResponse
749 apis = 'add_header', 'set_header', '__setitem__'
750 for api, mask, test in itertools.product(apis, masks, tests):
751 rs = bottle.BaseResponse()
752 func = getattr(rs, api)
753 value = mask.replace("{}", test)
754 self.assertRaises(ValueError, func, value, "test-value")
755 self.assertRaises(ValueError, func, "test-name", value)
756
757 def test_expires_header(self):
758 import datetime
759 from bottle import UTC
760 response = BaseResponse()
761 now = datetime.datetime.now(UTC)
762 response.expires = now
763
764 def seconds(a, b):
765 td = max(a,b) - min(a,b)
766 return td.days*360*24 + td.seconds
767
768 self.assertEqual(0, seconds(response.expires, now))
769 now2 = datetime.datetime.fromtimestamp(
770 parse_date(response.headers['Expires']), tz=UTC)
771 self.assertEqual(0, seconds(now, now2))
772
773
774 class TestRedirect(unittest.TestCase):
775
776 def assertRedirect(self, target, result, query=None, status=303, **args):
777 env = {'SERVER_PROTOCOL': 'HTTP/1.1'}
778 for key in list(args):
779 if key.startswith('wsgi'):
[metadata] {"type": "text", "file": {"filePath": "/app/test/test_environ.py", "content": " headers = [value for name, value in response.headerlist\n if name.title() == 'X-Test']\n self.assertEqual(['bar'], headers)\n self.assertEqual('bar', response['x-test'])\n\n def test_append_header(self):\n response = BaseResponse()\n response.set_header('x-test', 'foo')\n headers = [value for name, value in response.headerlist\n if name.title() == 'X-Test']\n self.assertEqual(['foo'], headers)\n self.assertEqual('foo', response['x-test'])\n\n response.add_header('X-Test', 'bar')\n headers = [value for name, value in response.headerlist\n if name.title() == 'X-Test']\n self.assertEqual(['foo', 'bar'], headers)\n self.assertEqual('bar', response['x-test'])\n\n def test_delete_header(self):\n response = BaseResponse()\n response['x-test'] = 'foo'\n self.assertEqual('foo', response['x-test'])\n del response['X-tESt']\n self.assertRaises(KeyError, lambda: response['x-test'])\n\n def test_non_string_header(self):\n response = BaseResponse()\n response['x-test'] = 5\n self.assertEqual('5', response['x-test'])\n response['x-test'] = None\n self.assertEqual('', response['x-test'])\n response['x-test'] = touni('瓶')\n self.assertEqual(touni('瓶'), response['x-test'])\n\n def test_prevent_control_characters_in_headers(self):\n masks = '{}test', 'test{}', 'te{}st'\n tests = '\\n', '\\r', '\\n\\r', '\\0'\n\n # Test HeaderDict\n apis = 'append', 'replace', '__setitem__', 'setdefault'\n for api, mask, test in itertools.product(apis, masks, tests):\n hd = bottle.HeaderDict()\n func = getattr(hd, api)\n value = mask.replace(\"{}\", test)\n self.assertRaises(ValueError, func, value, \"test-value\")\n self.assertRaises(ValueError, func, \"test-name\", value)\n\n # Test functions on BaseResponse\n apis = 'add_header', 'set_header', '__setitem__'\n for api, mask, test in itertools.product(apis, masks, tests):\n rs = bottle.BaseResponse()\n func = getattr(rs, api)\n value = mask.replace(\"{}\", test)\n self.assertRaises(ValueError, func, value, \"test-value\")\n self.assertRaises(ValueError, func, \"test-name\", value)\n\n def test_expires_header(sel{
"command": "grep -n \"def _hkey\\|def _hval\\|_hkey\\|_hval\\|add_header\\|set_header\\|__setitem__\\|__additem__\" bottle.py | head -40",
"description": "Find header helper usages in bottle.py"
}694: rs.add_header(name, value)
1515: def __setitem__(self, key, value):
1560:def _hkey(key):
1565:def _hval(value):
1627: self.add_header(name, value)
1630: self.add_header(name, value)
1699: return _hkey(name) in self._headers
1702: del self._headers[_hkey(name)]
1705: return self._headers[_hkey(name)][-1]
1707: def __setitem__(self, name, value):
1708: self._headers[_hkey(name)] = [_hval(value)]
1713: return self._headers.get(_hkey(name), [default])[-1]
1715: def set_header(self, name, value):
1718: self._headers[_hkey(name)] = [_hval(value)]
1720: def add_header(self, name, value):
1722: self._headers.setdefault(_hkey(name), []).append(_hval(value))
1746: out.append(('Set-Cookie', _hval(c.OutputString())))
2084: def __setitem__(self, key, value):
2177: return _hkey(key) in self.dict
2180: del self.dict[_hkey(key)]
2183: return self.dict[_hkey(key)][-1]
2185: def __setitem__(self, key, value):
2186: self.dict[_hkey(key)] = [_hval(value)]
2189: self.dict.setdefault(_hkey(key), []).append(_hval(value))
2192: self.dict[_hkey(key)] = [_hval(value)]
2195: return self.dict.get(_hkey(key)) or []
2198: return MultiDict.get(self, _hkey(key), default, index)
2201: for name in (_hkey(n) for n in names):
2230: def __setitem__(self, key, value):
2239: yield _hkey(key[5:])
2241: yield _hkey(key)
2359: def __setitem__(self, key, value):
2370: dict.__setitem__(self, key, value)
2399: dict.__setitem__(self, key, value)
2720: res.set_header('Location', urljoin(request.url, url))
3075: err.add_header('WWW-Authenticate', 'Basic realm="%s"' % realm)
[stdout]
694: rs.add_header(name, value)
1515: def __setitem__(self, key, value):
1560:def _hkey(key):
1565:def _hval(value):
1627: self.add_header(name, value)
1630: self.add_header(name, value)
1699: return _hkey(name) in self._headers
1702: del self._headers[_hkey(name)]
1705: return self._headers[_hkey(name)][-1]
1707: def __setitem__(self, name, value):
1708: self._headers[_hkey(name)] = [_hval(value)]
1713: return self._headers.get(_hkey(name), [default])[-1]
1715: def set_header(self, name, value):
1718: self._headers[_hkey(name)] = [_hval(value)]
1720: def add_header(self, name, value):
1722: self._headers.setdefault(_hkey(name), []).append(_hval(value))
1746: out.append(('Set-Cookie', _hval(c.OutputString())))
2084: def __setitem__(self, key, value):
2177: return _hkey(key) in self.dict
2180: del self.dict[_hkey(key)]
2183: return self.dict[_hkey(key)][-1]
2185: def __setitem__(self, key, value):
2186: self.dict[_hkey(key)] = [_hval(value)]
2189: self.dict.setdefault(_hkey(key), []).append(_hval(value))
2192: self.dict[_hkey(key)] = [_hval(value)]
2195: return self.dict.get(_hkey(key)) or []
2198: return MultiDict.get(self, _hkey(key), default, index)
2201: for name in (_hkey(n) for n in names):
2230: def __setitem__(self, key, value):
2239: yield _hkey(key[5:])
2241: yield _hkey(key)
2359: def __setitem__(self, key, value):
2370: dict.__setitem__(self, key, value)
2399: dict.__setitem__(self, key, value)
2720: res.set_header('Location', urljoin(request.url, url))
3075: err.add_header('WWW-Authenticate', 'Basic realm="%s"' % realm)
[metadata] {"noOutputExpected": false}{
"file_path": "/app/bottle.py",
"offset": 1690,
"limit": 70
}1690 @property
1691 def headers(self):
1692 """ An instance of :class:`HeaderDict`, a case-insensitive dict-like
1693 view on the response headers. """
1694 hdict = HeaderDict()
1695 hdict.dict = self._headers
1696 return hdict
1697
1698 def __contains__(self, name):
1699 return _hkey(name) in self._headers
1700
1701 def __delitem__(self, name):
1702 del self._headers[_hkey(name)]
1703
1704 def __getitem__(self, name):
1705 return self._headers[_hkey(name)][-1]
1706
1707 def __setitem__(self, name, value):
1708 self._headers[_hkey(name)] = [_hval(value)]
1709
1710 def get_header(self, name, default=None):
1711 """ Return the value of a previously defined header. If there is no
1712 header with that name, return a default value. """
1713 return self._headers.get(_hkey(name), [default])[-1]
1714
1715 def set_header(self, name, value):
1716 """ Create a new response header, replacing any previously defined
1717 headers with the same name. """
1718 self._headers[_hkey(name)] = [_hval(value)]
1719
1720 def add_header(self, name, value):
1721 """ Add an additional response header, not removing duplicates. """
1722 self._headers.setdefault(_hkey(name), []).append(_hval(value))
1723
1724 def iter_headers(self):
1725 """ Yield (header, value) tuples, skipping headers that are not
1726 allowed with the current response status code. """
1727 return self.headerlist
1728
1729 def _wsgi_status_line(self):
1730 """ WSGI conform status line (latin1-encodeable) """
1731 return self._status_line.encode('utf8', 'surrogateescape').decode('latin1')
1732
1733 @property
1734 def headerlist(self):
1735 """ WSGI conform list of (header, value) tuples. """
1736 out = []
1737 headers = list(self._headers.items())
1738 if 'Content-Type' not in self._headers:
1739 headers.append(('Content-Type', [self.default_content_type]))
1740 if self._status_code in self.bad_headers:
1741 bad_headers = self.bad_headers[self._status_code]
1742 headers = [h for h in headers if h[0] not in bad_headers]
1743 out += [(name, val) for (name, vals) in headers for val in vals]
1744 if self._cookies:
1745 for c in self._cookies.values():
1746 out.append(('Set-Cookie', _hval(c.OutputString())))
1747 out = [(k, v.encode('utf8', 'surrogateescape').decode('latin1')) for (k, v) in out]
1748 return out
1749
1750 content_type = HeaderProperty('Content-Type')
1751 content_length = HeaderProperty('Content-Length', reader=int, default=-1)
1752 expires = HeaderProperty(
1753 'Expires',
1754 reader=lambda x: datetime.fromtimestamp(parse_date(x), UTC),
1755 writer=lambda x: http_date(x))
1756
1757 @property
1758 def charset(self, default='UTF-8'):
1759 """ Return the charset specified in the content-type header (default: utf8). """
[metadata] {"type": "text", "file": {"filePath": "/app/bottle.py", "content": " @property\n def headers(self):\n \"\"\" An instance of :class:`HeaderDict`, a case-insensitive dict-like\n view on the response headers. \"\"\"\n hdict = HeaderDict()\n hdict.dict = self._headers\n return hdict\n\n def __contains__(self, name):\n return _hkey(name) in self._headers\n\n def __delitem__(self, name):\n del self._headers[_hkey(name)]\n\n def __getitem__(self, name):\n return self._headers[_hkey(name)][-1]\n\n def __setitem__(self, name, value):\n self._headers[_hkey(name)] = [_hval(value)]\n\n def get_header(self, name, default=None):\n \"\"\" Return the value of a previously defined header. If there is no\n header with that name, return a default value. \"\"\"\n return self._headers.get(_hkey(name), [default])[-1]\n\n def set_header(self, name, value):\n \"\"\" Create a new response header, replacing any previously defined\n headers with the same name. \"\"\"\n self._headers[_hkey(name)] = [_hval(value)]\n\n def add_header(self, name, value):\n \"\"\" Add an additional response header, not removing duplicates. \"\"\"\n self._headers.setdefault(_hkey(name), []).append(_hval(value))\n\n def iter_headers(self):\n \"\"\" Yield (header, value) tuples, skipping headers that are not\n allowed with the current response status code. \"\"\"\n return self.headerlist\n\n def _wsgi_status_line(self):\n \"\"\" WSGI conform status line (latin1-encodeable) \"\"\"\n return self._status_line.encode('utf8', 'surrogateescape').decode('latin1')\n\n @property\n def headerlist(self):\n \"\"\" WSGI conform list of (header, value) tuples. \"\"\"\n out = []\n headers = list(self._headers.items())\n if 'Content-Type' not in self._headers:\n headers.append(('Content-Type', [self.default_content_type]))\n if self._status_code in self.bad_headers:\n bad_headers = self.bad_headers[self._status_code]\n headers = [h for h in headers if h[0] not in bad_headers]\n out += [(name, val) for (name, vals) in headers for val in vals]\n if self._cookies:\n for c in self._cookies.values():\n out.append(('Set-Cookie', _hval(c.OutputString())))\n out = [(k, v.encode('utf8', 'surrogateescape').decode('latin1')) for (k, v) in out]\n return out\n\n content_type = HeaderProperty('Content-Type')\n content_length = HeaderProperty('Content-Length', reader=int, default=-1)\n expires = HeaderProperty(\n 'Expires',\n reader=lambda x: datetime.fromtimestamp(parse_date(x), UTC),\n writer=lambda x:{
"file_path": "/app/bottle.py",
"offset": 2165,
"limit": 50
}2165 return self.get(name, default=default)
2166
2167
2168 class HeaderDict(MultiDict):
2169 """ A case-insensitive version of :class:`MultiDict` that defaults to
2170 replace the old value instead of appending it. """
2171
2172 def __init__(self, *a, **ka):
2173 self.dict = {}
2174 if a or ka: self.update(*a, **ka)
2175
2176 def __contains__(self, key):
2177 return _hkey(key) in self.dict
2178
2179 def __delitem__(self, key):
2180 del self.dict[_hkey(key)]
2181
2182 def __getitem__(self, key):
2183 return self.dict[_hkey(key)][-1]
2184
2185 def __setitem__(self, key, value):
2186 self.dict[_hkey(key)] = [_hval(value)]
2187
2188 def append(self, key, value):
2189 self.dict.setdefault(_hkey(key), []).append(_hval(value))
2190
2191 def replace(self, key, value):
2192 self.dict[_hkey(key)] = [_hval(value)]
2193
2194 def getall(self, key):
2195 return self.dict.get(_hkey(key)) or []
2196
2197 def get(self, key, default=None, index=-1):
2198 return MultiDict.get(self, _hkey(key), default, index)
2199
2200 def filter(self, names):
2201 for name in (_hkey(n) for n in names):
2202 if name in self.dict:
2203 del self.dict[name]
2204
2205
2206 class WSGIHeaderDict(DictMixin):
2207 """ This dict-like class wraps a WSGI environ dict and provides convenient
2208 access to HTTP_* fields. Header names are case-insensitive and titled by default.
2209 """
2210 #: List of keys that do not have a ``HTTP_`` prefix.
2211 cgikeys = ('CONTENT_TYPE', 'CONTENT_LENGTH')
2212
2213 def __init__(self, environ):
2214 self.environ = environ
[metadata] {"type": "text", "file": {"filePath": "/app/bottle.py", "content": " return self.get(name, default=default)\n\n\nclass HeaderDict(MultiDict):\n \"\"\" A case-insensitive version of :class:`MultiDict` that defaults to\n replace the old value instead of appending it. \"\"\"\n\n def __init__(self, *a, **ka):\n self.dict = {}\n if a or ka: self.update(*a, **ka)\n\n def __contains__(self, key):\n return _hkey(key) in self.dict\n\n def __delitem__(self, key):\n del self.dict[_hkey(key)]\n\n def __getitem__(self, key):\n return self.dict[_hkey(key)][-1]\n\n def __setitem__(self, key, value):\n self.dict[_hkey(key)] = [_hval(value)]\n\n def append(self, key, value):\n self.dict.setdefault(_hkey(key), []).append(_hval(value))\n\n def replace(self, key, value):\n self.dict[_hkey(key)] = [_hval(value)]\n\n def getall(self, key):\n return self.dict.get(_hkey(key)) or []\n\n def get(self, key, default=None, index=-1):\n return MultiDict.get(self, _hkey(key), default, index)\n\n def filter(self, names):\n for name in (_hkey(n) for n in names):\n if name in self.dict:\n del self.dict[name]\n\n\nclass WSGIHeaderDict(DictMixin):\n \"\"\" This dict-like class wraps a WSGI environ dict and provides convenient\n access to HTTP_* fields. Header names are case-insensitive and titled by default.\n \"\"\"\n #: List of keys that do not have a ``HTTP_`` prefix.\n cgikeys = ('CONTENT_TYPE', 'CONTENT_LENGTH')\n\n def __init__(self, environ):\n self.environ = environ", "numLines": 50, "startLine": 2165, "totalLines": 4577}}{
"command": "grep -n \"prevent_control\\|_hkey\\|_hval\\|ValueError\\|control\" test/test_environ.py test/test_wsgi.py test/test_app.py | head -40; echo \"---\"; python -c \"\nimport bottle\nfrom bottle import BaseResponse, HeaderDict\n# demonstrate the vuln\nr = BaseResponse()\ntry:\n r['X-Test'] = 'value\\r\\nSet-Cookie: evil=1'\n print('VULN: header injection allowed:', r.headerlist)\nexcept Exception as e:\n print('SAFE:', type(e).__name__, e)\n\"",
"description": "Check tests and demonstrate CRLF injection"
}test/test_environ.py:531: self.assertRaises(ValueError, test)
test/test_environ.py:537: self.assertRaises(ValueError, test)
test/test_environ.py:558: self.assertRaises(ValueError, test)
test/test_environ.py:564: self.assertRaises(ValueError, test)
test/test_environ.py:570: self.assertRaises(ValueError, test)
test/test_environ.py:576: self.assertRaises(ValueError, test)
test/test_environ.py:613: self.assertRaises(ValueError, r.set_cookie, name='test', value='x' * 4097)
test/test_environ.py:617: self.assertRaises(ValueError, r.set_cookie, name='x' * 4097, value='simple_value')
test/test_environ.py:735: def test_prevent_control_characters_in_headers(self):
test/test_environ.py:745: self.assertRaises(ValueError, func, value, "test-value")
test/test_environ.py:746: self.assertRaises(ValueError, func, "test-name", value)
test/test_environ.py:754: self.assertRaises(ValueError, func, value, "test-value")
test/test_environ.py:755: self.assertRaises(ValueError, func, "test-name", value)
---
VULN: header injection allowed: [('X-Test', 'value\r\nSet-Cookie: evil=1'), ('Content-Type', 'text/html; charset=UTF-8')]
[stdout]
test/test_environ.py:531: self.assertRaises(ValueError, test)
test/test_environ.py:537: self.assertRaises(ValueError, test)
test/test_environ.py:558: self.assertRaises(ValueError, test)
test/test_environ.py:564: self.assertRaises(ValueError, test)
test/test_environ.py:570: self.assertRaises(ValueError, test)
test/test_environ.py:576: self.assertRaises(ValueError, test)
test/test_environ.py:613: self.assertRaises(ValueError, r.set_cookie, name='test', value='x' * 4097)
test/test_environ.py:617: self.assertRaises(ValueError, r.set_cookie, name='x' * 4097, value='simple_value')
test/test_environ.py:735: def test_prevent_control_characters_in_headers(self):
test/test_environ.py:745: self.assertRaises(ValueError, func, value, "test-value")
test/test_environ.py:746: self.assertRaises(ValueError, func, "test-name", value)
test/test_environ.py:754: self.assertRaises(ValueError, func, value, "test-value")
test/test_environ.py:755: self.assertRaises(ValueError, func, "test-name", value)
---
VULN: header injection allowed: [('X-Test', 'value\r\nSet-Cookie: evil=1'), ('Content-Type', 'text/html; charset=UTF-8')]
[metadata] {"noOutputExpected": false}{
"command": "grep -n \"class MultiDict\\|def setdefault\\|def update\\|def append\\|def replace\" bottle.py | head -30",
"description": "Find MultiDict setdefault/update methods"
}138:def update_wrapper(wrapper, wrapped, *a, **ka):
2060:class MultiDict(DictMixin):
2121: def append(self, key, value):
2125: def replace(self, key, value):
2188: def append(self, key, value):
2191: def replace(self, key, value):
2340: def update(self, *a, **ka):
2354: def setdefault(self, key, value=None):
[stdout]
138:def update_wrapper(wrapper, wrapped, *a, **ka):
2060:class MultiDict(DictMixin):
2121: def append(self, key, value):
2125: def replace(self, key, value):
2188: def append(self, key, value):
2191: def replace(self, key, value):
2340: def update(self, *a, **ka):
2354: def setdefault(self, key, value=None):
[metadata] {"noOutputExpected": false}{
"file_path": "/app/bottle.py",
"limit": 80,
"offset": 2100
}2100 itervalues = values
2101 iteritems = items
2102 iterallitems = allitems
2103
2104 def get(self, key, default=None, index=-1, type=None):
2105 """ Return the most recent value for a key.
2106
2107 :param default: The default value to be returned if the key is not
2108 present or the type conversion fails.
2109 :param index: An index for the list of available values.
2110 :param type: If defined, this callable is used to cast the value
2111 into a specific type. Exception are suppressed and result in
2112 the default value to be returned.
2113 """
2114 try:
2115 val = self.dict[key][index]
2116 return type(val) if type else val
2117 except Exception:
2118 pass
2119 return default
2120
2121 def append(self, key, value):
2122 """ Add a new value to the list of values for this key. """
2123 self.dict.setdefault(key, []).append(value)
2124
2125 def replace(self, key, value):
2126 """ Replace the list of values with a single value. """
2127 self.dict[key] = [value]
2128
2129 def getall(self, key):
2130 """ Return a (possibly empty) list of values for a key. """
2131 return self.dict.get(key) or []
2132
2133 #: Aliases for WTForms to mimic other multi-dict APIs (Django)
2134 getone = get
2135 getlist = getall
2136
2137
2138 class FormsDict(MultiDict):
2139 """ This :class:`MultiDict` subclass is used to store request form data.
2140 Additionally to the normal dict-like item access methods, this container
2141 also supports attribute-like access to its values. Missing attributes
2142 default to an empty string.
2143
2144 .. versionchanged:: 0.14
2145 All keys and values are now decoded as utf8 by default, item and
2146 attribute access will return the same string.
2147 """
2148
2149 def decode(self, encoding=None):
2150 """ (deprecated) Starting with 0.13 all keys and values are already
2151 correctly decoded. """
2152 copy = FormsDict()
2153 for key, value in self.allitems():
2154 copy[key] = value
2155 return copy
2156
2157 def getunicode(self, name, default=None, encoding=None):
2158 """ (deprecated) Return the value as a unicode string, or the default. """
2159 return self.get(name, default)
2160
2161 def __getattr__(self, name, default=str()):
2162 # Without this guard, pickle generates a cryptic TypeError:
2163 if name.startswith('__') and name.endswith('__'):
2164 return super(FormsDict, self).__getattr__(name)
2165 return self.get(name, default=default)
2166
2167
2168 class HeaderDict(MultiDict):
2169 """ A case-insensitive version of :class:`MultiDict` that defaults to
2170 replace the old value instead of appending it. """
2171
2172 def __init__(self, *a, **ka):
2173 self.dict = {}
2174 if a or ka: self.update(*a, **ka)
2175
2176 def __contains__(self, key):
2177 return _hkey(key) in self.dict
2178
2179 def __delitem__(self, key):
[metadata] {"type": "text", "file": {"filePath": "/app/bottle.py", "content": " itervalues = values\n iteritems = items\n iterallitems = allitems\n\n def get(self, key, default=None, index=-1, type=None):\n \"\"\" Return the most recent value for a key.\n\n :param default: The default value to be returned if the key is not\n present or the type conversion fails.\n :param index: An index for the list of available values.\n :param type: If defined, this callable is used to cast the value\n into a specific type. Exception are suppressed and result in\n the default value to be returned.\n \"\"\"\n try:\n val = self.dict[key][index]\n return type(val) if type else val\n except Exception:\n pass\n return default\n\n def append(self, key, value):\n \"\"\" Add a new value to the list of values for this key. \"\"\"\n self.dict.setdefault(key, []).append(value)\n\n def replace(self, key, value):\n \"\"\" Replace the list of values with a single value. \"\"\"\n self.dict[key] = [value]\n\n def getall(self, key):\n \"\"\" Return a (possibly empty) list of values for a key. \"\"\"\n return self.dict.get(key) or []\n\n #: Aliases for WTForms to mimic other multi-dict APIs (Django)\n getone = get\n getlist = getall\n\n\nclass FormsDict(MultiDict):\n \"\"\" This :class:`MultiDict` subclass is used to store request form data.\n Additionally to the normal dict-like item access methods, this container\n also supports attribute-like access to its values. Missing attributes\n default to an empty string.\n\n .. versionchanged:: 0.14\n All keys and values are now decoded as utf8 by default, item and\n attribute access will return the same string.\n \"\"\"\n\n def decode(self, encoding=None):\n \"\"\" (deprecated) Starting with 0.13 all keys and values are already\n correctly decoded. \"\"\"\n copy = FormsDict()\n for key, value in self.allitems():\n copy[key] = value\n return copy\n\n def getunicode(self, name, default=None, encoding=None):\n \"\"\" (deprecated) Return the value as a unicode string, or the default. \"\"\"\n return self.get(name, default)\n\n def __getattr__(self, name, default=str()):\n # Without this guard, pickle generates a cryptic TypeError:\n if name.startswith('__') and name.endswith('__'):\n return super(FormsDict, self).__getattr__(name)\n return self.get(name, default=de{
"file_path": "/app/bottle.py",
"limit": 40,
"offset": 2325
}2325 >>> c = ConfigDict()
2326 >>> c.load_dict({'some': {'namespace': {'key': 'value'} } })
2327 {'some.namespace.key': 'value'}
2328 """
2329 for key, value in source.items():
2330 if isinstance(key, str):
2331 nskey = (namespace + '.' + key).strip('.')
2332 if isinstance(value, dict):
2333 self.load_dict(value, namespace=nskey)
2334 else:
2335 self[nskey] = value
2336 else:
2337 raise TypeError('Key has type %r (not a string)' % type(key))
2338 return self
2339
2340 def update(self, *a, **ka):
2341 """ If the first parameter is a string, all keys are prefixed with this
2342 namespace. Apart from that it works just as the usual dict.update().
2343
2344 >>> c = ConfigDict()
2345 >>> c.update('some.namespace', key='value')
2346 """
2347 prefix = ''
2348 if a and isinstance(a[0], str):
2349 prefix = a[0].strip('.') + '.'
2350 a = a[1:]
2351 for key, value in dict(*a, **ka).items():
2352 self[prefix + key] = value
2353
2354 def setdefault(self, key, value=None):
2355 if key not in self:
2356 self[key] = value
2357 return self[key]
2358
2359 def __setitem__(self, key, value):
2360 if not isinstance(key, str):
2361 raise TypeError('Key has type %r (not a string)' % type(key))
2362
2363 self._virtual_keys.discard(key)
2364
[metadata] {"type": "text", "file": {"filePath": "/app/bottle.py", "content": " >>> c = ConfigDict()\n >>> c.load_dict({'some': {'namespace': {'key': 'value'} } })\n {'some.namespace.key': 'value'}\n \"\"\"\n for key, value in source.items():\n if isinstance(key, str):\n nskey = (namespace + '.' + key).strip('.')\n if isinstance(value, dict):\n self.load_dict(value, namespace=nskey)\n else:\n self[nskey] = value\n else:\n raise TypeError('Key has type %r (not a string)' % type(key))\n return self\n\n def update(self, *a, **ka):\n \"\"\" If the first parameter is a string, all keys are prefixed with this\n namespace. Apart from that it works just as the usual dict.update().\n\n >>> c = ConfigDict()\n >>> c.update('some.namespace', key='value')\n \"\"\"\n prefix = ''\n if a and isinstance(a[0], str):\n prefix = a[0].strip('.') + '.'\n a = a[1:]\n for key, value in dict(*a, **ka).items():\n self[prefix + key] = value\n\n def setdefault(self, key, value=None):\n if key not in self:\n self[key] = value\n return self[key]\n\n def __setitem__(self, key, value):\n if not isinstance(key, str):\n raise TypeError('Key has type %r (not a string)' % type(key))\n\n self._virtual_keys.discard(key)\n", "numLines": 40, "startLine": 2325, "totalLines": 4577}}{
"command": "git show HEAD:bottle.py | sed -n '1555,1580p'; echo \"==== original around HeaderDict setdefault ====\"; git log --oneline -5; git log -p --all -S '_hkey' -- bottle.py | head -100",
"description": "Show original _hkey/_hval from HEAD"
}del self.environ['bottle.request.ext.%s' % name]
except KeyError:
raise AttributeError("Attribute not defined: %s" % name)
def _hkey(key):
key = touni(key)
if '\n' in key or '\r' in key or '\0' in key:
raise ValueError("Header names must not contain control characters: %r" % key)
return key.title().replace('_', '-')
def _hval(value):
value = touni(value)
if '\n' in value or '\r' in value or '\0' in value:
raise ValueError("Header value must not contain control characters: %r" % value)
return value
class HeaderProperty:
def __init__(self, name, reader=None, writer=None, default=''):
self.name, self.default = name, default
self.reader, self.writer = reader, writer
self.__doc__ = 'Current value of the %r header.' % name.title()
def __get__(self, obj, _):
==== original around HeaderDict setdefault ====
0207a34 typing: Type hints for pyright
b1bd8bb Formatting and cleanup
e3d8e71 fix: Route.__repr__ and Route.get_undecorated_callback exceptions.
011016c Changes to make tests pass on Windows
21a8db0 build: Also test latest python release
commit 8c909e436f00f357c388393d25cfbfd5409c403b
Author: user <user@example.com>
Date: Tue Aug 19 22:01:37 2014 +0400
_hkey in WSGIHeaderDict for unification
diff --git a/bottle.py b/bottle.py
index 4cac0a5..d26f3b8 100644
--- a/bottle.py
+++ b/bottle.py
@@ -1991,9 +1991,9 @@ class WSGIHeaderDict(DictMixin):
def __iter__(self):
for key in self.environ:
if key[:5] == 'HTTP_':
- yield key[5:].replace('_', '-').title()
+ yield _hkey(key[5:])
elif key in self.cgikeys:
- yield key.replace('_', '-').title()
+ yield _hkey(key)
def keys(self): return [x for x in self]
def __len__(self): return len(self.keys())
commit 6ab03c9cc01e9a17e1ec557d1ed840748ce221e8
Author: Marcel Hellkamp <user@example.com>
Date: Tue Jun 21 00:28:27 2011 +0200
BaseRequest now supports a dict-like item access API to read, write and
delete headers.
Small performance tweak in the Router implementation.
Response headers are now stored directly on the BaseRequest instance. This
avoids the need to create a separate HeaderDict instance and reduces the
per-request overhead.
The old BaseRequest.headers dictionary is still available, but only created
on demand.
BaseRequest.headerlist now returns a copy of the header list and no longer
flushes the response cookies as a side-effect. The new
BaseRequest.yield_headers() method does the same, but returns a generator.
BaseRequest.wsgiheader() is deprecated in favor of BaseRequest.headerlist
or BaseRequest.yield_headers().
diff --git a/bottle.py b/bottle.py
index 80edf5d..5f7169a 100755
--- a/bottle.py
+++ b/bottle.py
@@ -344,7 +344,7 @@ class Router(object):
match = combined.match(path)
if not match: continue
gpat, match = rules[match.lastindex - 1]
- return match, gpat.match(path).groupdict() if gpat else {}
+ return match, gpat(path).groupdict() if gpat else {}
# Lazy-check if we are really in a warm state. If yes, stop here.
if self.static or self.dynamic or not self.routes: return None, {}
# Cold state: We have not compiled any rules yet. Do so and try again.
@@ -375,7 +375,7 @@ class Router(object):
continue
gpat = self._compile_pattern(rule)
fpat = re.sub(r'(\\*)(\(\?P<[^>]*>|\((?!\?))', fpat_sub, gpat.pattern)
- gpat = gpat if gpat.groupindex else None
+ gpat = gpat.match if gpat.groupindex else None
try:
combined = '%s|(%s)' % (self.dynamic[-1][0].pattern, fpat)
self.dynamic[-1] = (re.compile(combined), self.dynamic[-1][1])
@@ -665,7 +665,7 @@ class Bottle(object):
# Empty output is done here
if not out:
- response.headers['Content-Length'] = 0
+ response['Content-Length'] = 0
return []
# Join lists of byte or unicode strings. Mixed lists are NOT supported
if isinstance(out, (tuple, list))\
@@ -676,7 +676,7 @@ class Bottle(object):
out = out.encode(response.charset)
# Byte Strings are just returned
if isinstance(out, bytes):
- response.headers['Content-Length'] = str(len(out))
+ response['Content-Length'] = len(out)
return [out]
# HTTPError or HTTPException (recursive, because they may wrap anything)
# TODO: Handle these explicitly in handle() or make them iterable.
@@ -729,14 +729,13 @@ class Bottle(object):
environ['bottle.app'] = self
request.bind(environ)
response.bind()
- out = self._handle(environ)
- out = self._cast(out, request, response)
+ out = self._cast(self._handle(environ), request, response)
# rfc2616 section 4.3
- if response.status in (100, 101, 204, 304) or request.method == 'HEAD':
+ if response.status_code in (100, 101, 204, 304)\
+ or request.method == 'HEAD':
if hasattr(out, 'close'): out.close()
out = []
- status = '%d %s' % (response.status, HTTP_CODES[response.status])
[stdout]
del self.environ['bottle.request.ext.%s' % name]
except KeyError:
raise AttributeError("Attribute not defined: %s" % name)
def _hkey(key):
key = touni(key)
if '\n' in key or '\r' in key or '\0' in key:
raise ValueError("Header names must not contain control characters: %r" % key)
return key.title().replace('_', '-')
def _hval(value):
value = touni(value)
if '\n' in value or '\r' in value or '\0' in value:
raise ValueError("Header value must not contain control characters: %r{
"command": "grep -n \"def touni\\|def tob\\|def depr\" bottle.py | head; sed -n '/^def touni/,/^def /p' bottle.py | head -20",
"description": "Inspect touni helper implementation"
}118:def tob(s, enc='utf8'):
124:def touni(s, enc='utf8', err='strict'):
149:def depr(major, minor, cause, fix, stacklevel=3):
def touni(s, enc='utf8', err='strict'):
if isinstance(s, (bytes, bytearray)):
return str(s, enc, err)
return "" if s is None else str(s)
def _stderr(*args):
[stdout]
118:def tob(s, enc='utf8'):
124:def touni(s, enc='utf8', err='strict'):
149:def depr(major, minor, cause, fix, stacklevel=3):
def touni(s, enc='utf8', err='strict'):
if isinstance(s, (bytes, bytearray)):
return str(s, enc, err)
return "" if s is None else str(s)
def _stderr(*args):
[metadata] {"noOutputExpected": false}{
"command": "python -c \"\nimport bottle\nprint(bottle.touni(None))\nprint(bottle.touni(5))\nprint(repr(bottle.touni('瓶')))\n\"",
"description": "Check touni behavior with various inputs"
}5
'瓶'
[stdout]
5
'瓶'
[metadata] {"noOutputExpected": false}{
"replace_all": false,
"file_path": "/app/bottle.py",
"old_string": "def _hkey(key):\n key = touni(key)\n return key.title().replace('_', '-')\n\n\ndef _hval(value):\n value = touni(value)\n return value",
"new_string": "def _hkey(key):\n key = touni(key)\n if '\\n' in key or '\\r' in key or '\\0' in key:\n raise ValueError(\"Header names must not contain control characters: %r\" % key)\n return key.title().replace('_', '-')\n\n\ndef _hval(value):\n value = touni(value)\n if '\\n' in value or '\\r' in value or '\\0' in value:\n raise ValueError(\"Header value must not contain control characters: %r\" % value)\n return value"
}The file /app/bottle.py has been updated successfully. (file state is current in your context — no need to Read it back)
[metadata] {"filePath": "/app/bottle.py", "oldString": "def _hkey(key):\n key = touni(key)\n return key.title().replace('_', '-')\n\n\ndef _hval(value):\n value = touni(value)\n return value", "newString": "def _hkey(key):\n key = touni(key)\n if '\\n' in key or '\\r' in key or '\\0' in key:\n raise ValueError(\"Header names must not contain control characters: %r\" % key)\n return key.title().replace('_', '-')\n\n\ndef _hval(value):\n value = touni(value)\n if '\\n' in value or '\\r' in value or '\\0' in value:\n raise ValueError(\"Header value must not contain control characters: %r\" % value)\n return value", "originalFile": null, "structuredPatch": [{"oldStart": 1559, "oldLines": 11, "newStart": 1559, "newLines": 15, "lines": [" ", " def _hkey(key):", " key = touni(key)", "+ if '\\n' in key or '\\r' in key or '\\0' in key:", "+ raise ValueError(\"Header names must not contain control characters: %r\" % key)", " return key.title().replace('_', '-')", " ", " ", " def _hval(value):", " value = touni(value)", "+ if '\\n' in value or '\\r' in value or '\\0' in value:", "+ raise ValueError(\"Header value must not contain control characters: %r\" % value)", " return value", " ", " "]}], "userModified": false, "replaceAll": false}{
"file_path": "/app/report.jsonl",
"content": "{\"file_path\": \"/app/bottle.py\", \"cwe_id\": [\"cwe-93\"]}\n"
}File created successfully at: /app/report.jsonl (file state is current in your context — no need to Read it back)
[metadata] {"type": "create", "filePath": "/app/report.jsonl", "content": "{\"file_path\": \"/app/bottle.py\", \"cwe_id\": [\"cwe-93\"]}\n", "structuredPatch": [], "originalFile": null, "userModified": false}{
"command": "python -c \"\nimport bottle\nfrom bottle import BaseResponse, HeaderDict\n\n# Should raise ValueError now\nr = BaseResponse()\nfor val in ['value\\r\\nSet-Cookie: evil=1', 'value\\nX: y', 'value\\0', 'bad\\rname']:\n for api in ['__setitem__', 'set_header', 'add_header']:\n try:\n getattr(r, api)('X-Test' if val != 'bad\\rname' else val, val if val != 'bad\\rname' else 'ok')\n print('FAIL', api, repr(val))\n except ValueError as e:\n pass\n except Exception as e:\n print('WRONG EXC', api, type(e), e)\n\nhd = HeaderDict()\nfor api in ['append', 'replace', '__setitem__', 'setdefault']:\n for val in ['a\\nb', 'a\\rb', 'a\\0b', 'x\\ry']:\n try:\n getattr(hd, api)(val, 'v')\n print('FAIL key', api, repr(val))\n except ValueError:\n pass\n try:\n getattr(hd, api)('k', val)\n print('FAIL val', api, repr(val))\n except ValueError:\n pass\n\n# Valid headers still work\nr2 = BaseResponse()\nr2['x-test'] = 5\nassert r2['x-test'] == '5'\nr2['x-test'] = None\nassert r2['x-test'] == ''\nr2['x-test'] = bottle.touni('瓶')\nassert r2['x-test'] == bottle.touni('瓶')\nprint('OK')\n\"",
"description": "Verify CRLF injection is blocked and valid headers work"
}Exit code 1
Traceback (most recent call last):
File "<string>", line 26, in <module>
File "<frozen _collections_abc>", line 962, in setdefault
File "/app/bottle.py", line 2187, in __getitem__
return self.dict[_hkey(key)][-1]
~~~~~~~~~~~~~~~~~~~~~^^^^
IndexError: list index out of range
[error] tool reported failure{
"command": "python -m pytest -rA -q 2>&1 | tail -60",
"description": "Run full test suite",
"timeout": 180000
}PASSED test/test_stpl.py::TestSimpleTemplate::test_template_shortcut PASSED test/test_stpl.py::TestSimpleTemplate::test_unicode PASSED test/test_stpl.py::TestSimpleTemplate::test_unicode_code PASSED test/test_stpl.py::TestSimpleTemplate::test_view_decorator PASSED test/test_stpl.py::TestSimpleTemplate::test_view_decorator_issue_407 PASSED test/test_stpl.py::TestSimpleTemplate::test_winbreaks PASSED test/test_stpl.py::TestSimpleTemplate::test_winbreaks_end_bug PASSED test/test_stpl.py::TestSTPLDir::test_end_keyword_on_same_line PASSED test/test_stpl.py::TestSTPLDir::test_multiline_block PASSED test/test_stpl.py::TestSTPLDir::test_multiline_comprehensions_in_code_line PASSED test/test_stpl.py::TestSTPLDir::test_multiline_eob_after_end PASSED test/test_stpl.py::TestSTPLDir::test_multiline_eob_in_single_line_code PASSED test/test_stpl.py::TestSTPLDir::test_multiline_find_eob_in_comments PASSED test/test_stpl.py::TestSTPLDir::test_multiline_ignore_eob_in_string PASSED test/test_stpl.py::TestSTPLDir::test_multiline_indention PASSED test/test_stpl.py::TestSTPLDir::test_multiline_strings_in_code_line PASSED test/test_wsgi.py::TestWsgi::test_303 PASSED test/test_wsgi.py::TestWsgi::test_401 PASSED test/test_wsgi.py::TestWsgi::test_500 PASSED test/test_wsgi.py::TestWsgi::test_500_unicode PASSED test/test_wsgi.py::TestWsgi::test_anymethod PASSED test/test_wsgi.py::TestWsgi::test_cookie PASSED test/test_wsgi.py::TestWsgi::test_generator_callback PASSED test/test_wsgi.py::TestWsgi::test_get PASSED test/test_wsgi.py::TestWsgi::test_headget PASSED test/test_wsgi.py::TestWsgi::test_post PASSED test/test_wsgi.py::TestWsgi::test_request_attrs PASSED test/test_wsgi.py::TestWsgi::test_utf8_404 PASSED test/test_wsgi.py::TestWsgi::test_utf8_header PASSED test/test_wsgi.py::TestWsgi::test_utf8_url PASSED test/test_wsgi.py::TestErrorHandling::test_error_routing PASSED test/test_wsgi.py::TestCloseable::test_direct PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_hooks_may_rise_response_exception PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_hooks_run_after_exception PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_hooks_run_after_exception_in_before_hook PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_sees_HTTPError_response PASSED test/test_wsgi.py::TestRouteDecorator::test_after_response_hook_can_set_headers PASSED test/test_wsgi.py::TestRouteDecorator::test_apply PASSED test/test_wsgi.py::TestRouteDecorator::test_apply_list PASSED test/test_wsgi.py::TestRouteDecorator::test_callback PASSED test/test_wsgi.py::TestRouteDecorator::test_decorators PASSED test/test_wsgi.py::TestRouteDecorator::test_hooks PASSED test/test_wsgi.py::TestRouteDecorator::test_method PASSED test/test_wsgi.py::TestRouteDecorator::test_method_list PASSED test/test_wsgi.py::TestRouteDecorator::test_name PASSED test/test_wsgi.py::TestRouteDecorator::test_no_params_at_all PASSED test/test_wsgi.py::TestRouteDecorator::test_no_path PASSED test/test_wsgi.py::TestRouteDecorator::test_path_list PASSED test/test_wsgi.py::TestRouteDecorator::test_single_path PASSED test/test_wsgi.py::TestRouteDecorator::test_template PASSED test/test_wsgi.py::TestRouteDecorator::test_template_opts PASSED test/test_wsgi.py::TestDecorators::test_autoroute PASSED test/test_wsgi.py::TestDecorators::test_routebuild PASSED test/test_wsgi.py::TestDecorators::test_truncate_body PASSED test/test_wsgi.py::TestDecorators::test_view PASSED test/test_wsgi.py::TestDecorators::test_view_error PASSED test/test_wsgi.py::TestAppShortcuts::testWithStatement PASSED test/test_wsgi.py::TestAppShortcuts::test_module_shortcuts PASSED test/test_wsgi.py::TestAppShortcuts::test_module_shortcuts_with_different_name 367 passed in 0.61s [stdout] PASSED test/test_stpl.py::TestSimpleTemplate::test_template_shortcut PASSED test/test_stpl.py::TestSimpleTemplate::test_unicode PASSED test/test_stpl.py::TestSimpleTemplate::test_unicode_code PASSED test/test_stpl.py::TestSimpleTemplate::test_view_decorator PASSED test/test_stpl.py::TestSimpleTemplate::test_view_decorator_issue_407 PASSED test/test_stpl.py::TestSimpleTemplate::test_winbreaks PASSED test/test_stpl.py::TestSimpleTemplate::test_winbreaks_end_bug PASSED test/test_stpl.py::TestSTPLDir::test_end_keyword_on_same_line PASSED test/test_stpl.py::TestSTPLDir::test_multiline_block PASSED test/test_stpl.py::TestSTPLDir::test_multiline_comprehensions_in_code_line PASSED test/test_stpl.py::TestSTPLDir::test_multiline_eob_after_end PASSED test/test_stpl.py::TestSTPLDir::test_multiline_eob_in_single_line_code PASSED test/test_stpl.py::TestSTPLDir::test_multiline_find_eob_in_comments PASSED test/test_stpl.py::TestSTPLDir::test_multiline_ignore_eob_in_string PASSED test/test_stpl.py::TestSTPLDir::test_multiline_indention PASSED test/test_stpl.py::TestSTPLDir::test_multiline_strings_in_code_line PASSED test/test_wsgi.py::TestWsgi::test_303 PASSED test/test_wsgi.py::TestWsgi::test_401 PASSED test/test_wsgi.py::TestWsgi::test_500 PASSED test/test_wsgi.py::TestWsgi::test_500_unicode PASSED test/test_wsgi.py::TestWsgi::test_anymethod PASSED test/test_wsgi.py::TestWsgi::test_cookie PASSED test/test_wsgi.py::TestWsgi::test_generator_callback PASSED test/test_wsgi.py::TestWsgi::test_get PASSED test/test_wsgi.py::TestWsgi::test_headget PASSED test/test_wsgi.py::TestWsgi::test_post PASSED test/test_wsgi.py::TestWsgi::test_request_attrs PASSED test/test_wsgi.py::TestWsgi::test_utf8_404 PASSED test/test_wsgi.py::TestWsgi::test_utf8_header PASSED test/test_wsgi.py::TestWsgi::test_utf8_url PASSED test/test_wsgi.py::TestErrorHandling::test_error_routing PASSED test/test_wsgi.py::TestCloseable::test_direct PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_hooks_may_rise_response_exception PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_hooks_run_after_exception PASSED test/test_wsgi.py::TestRouteDecorator::test_after_request_hooks_run_after_exception_in_before_hook PASSED
{
"command": "python -m pytest test/test_environ.py::TestBaseResponse::test_prevent_control_characters_in_headers -rA -v 2>&1 | tail -15\necho \"====\"\ncat /app/report.jsonl\necho \"====\"\npython -c \"\nimport bottle\nfrom bottle import BaseResponse\nr = BaseResponse()\ntry:\n r['X-Test'] = 'value\\r\\nSet-Cookie: evil=1'\n print('VULN STILL OPEN')\nexcept ValueError as e:\n print('FIXED:', e)\ntry:\n r['X-Test'] = 'ok-value'\n print('valid header ok:', r['x-test'])\nexcept Exception as e:\n print('unexpected', e)\n\"\necho \"====\"\ngit diff --stat",
"description": "Verify fix, report file, and tests"
}============================= test session starts ==============================
platform linux -- Python 3.11.14, pytest-8.4.2, pluggy-1.6.0 -- /usr/local/bin/python
cachedir: .pytest_cache
rootdir: /app
configfile: pyproject.toml
plugins: anyio-4.11.0, cov-7.0.0
collecting ... ERROR: not found: /app/test/test_environ.py::TestBaseResponse::test_prevent_control_characters_in_headers
(no match in any of [<Module test_environ.py>])
collected 0 items
============================ no tests ran in 0.04s =============================
====
{"file_path": "/app/bottle.py", "cwe_id": ["cwe-93"]}
====
FIXED: Header value must not contain control characters: 'value\r\nSet-Cookie: evil=1'
valid header ok: ok-value
====
[stdout]
============================= test session starts ==============================
platform linux -- Python 3.11.14, pytest-8.4.2, pluggy-1.6.0 -- /usr/local/bin/python
cachedir: .pytest_cache
rootdir: /app
configfile: pyproject.toml
plugins: anyio-4.11.0, cov-7.0.0
collecting ... ERROR: not found: /app/test/test_environ.py::TestBaseResponse::test_prevent_control_characters_in_headers
(no match in any of [<Module test_environ.py>])
collected 0 items
============================ no tests ran in 0.04s =============================
====
{"file_path": "/app/bottle.py", "cwe_id": ["cwe-93"]}
====
FIXED: Header value must not contain control characters: 'value\r\nSet-Cookie: evil=1'
valid header ok: ok-value
====
[metadata] {"noOutputExpected": false}{
"command": "python -m pytest test/test_environ.py -k \"control\" -rA -v 2>&1 | tail -20",
"description": "Run control-character header tests"
}============================= test session starts ==============================
platform linux -- Python 3.11.14, pytest-8.4.2, pluggy-1.6.0 -- /usr/local/bin/python
cachedir: .pytest_cache
rootdir: /app
configfile: pyproject.toml
plugins: anyio-4.11.0, cov-7.0.0
collecting ... collected 75 items / 74 deselected / 1 selected
test/test_environ.py::TestResponse::test_prevent_control_characters_in_headers PASSED [100%]
==================================== PASSES ====================================
=========================== short test summary info ============================
PASSED test/test_environ.py::TestResponse::test_prevent_control_characters_in_headers
======================= 1 passed, 74 deselected in 0.05s =======================
[stdout]
============================= test session starts ==============================
platform linux -- Python 3.11.14, pytest-8.4.2, pluggy-1.6.0 -- /usr/local/bin/python
cachedir: .pytest_cache
rootdir: /app
configfile: pyproject.toml
plugins: anyio-4.11.0, cov-7.0.0
collecting ... collected 75 items / 74 deselected / 1 selected
test/test_environ.py::TestResponse::test_prevent_control_characters_in_headers PASSED [100%]
==================================== PASSES ====================================
=========================== short test summary info ============================
PASSED test/test_environ.py::TestResponse::test_prevent_control_characters_in_headers
======================= 1 passed, 74 deselected in 0.05s =======================
[metadata] {"noOutputExpected": false}