From 9cdcf70fd98f2b0b4f9da0bd1e96e6c4a0a7ccb7 Mon Sep 17 00:00:00 2001 From: "Kostis Anagnostopoulos, Yoga-2" Date: Sat, 21 Feb 2015 00:28:51 +0100 Subject: [PATCH 1/4] Bump version from 1.1.7-beta.0 --> beta.1. --- pypiserver/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pypiserver/__init__.py b/pypiserver/__init__.py index d0fc2ad..3a95c5b 100644 --- a/pypiserver/__init__.py +++ b/pypiserver/__init__.py @@ -1,5 +1,5 @@ -__version_info__ = (1, 1, 7, 'beta.0') -version = __version__ = "1.1.7-beta.0" +__version_info__ = (1, 1, 7, 'beta.1') +version = __version__ = "1.1.7-beta.1" def app(root=None, From cb6f3b698aa78d15902a5d97e03ed7f4692d7c1d Mon Sep 17 00:00:00 2001 From: "Kostis Anagnostopoulos, Yoga-2" Date: Sat, 21 Feb 2015 00:34:37 +0100 Subject: [PATCH 2/4] Use bottle's SimpleTemplate engine to avoid XSS on welcome-page (#77). - Add 1 TC. - TODO: Probable XSS still in error-messages. --- pypiserver/_app.py | 19 ++++++++++--------- pypiserver/welcome.html | 10 +++++----- tests/sample_msg.html | 10 +++++----- tests/test_app.py | 7 +++++-- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/pypiserver/_app.py b/pypiserver/_app.py index f01a055..52807bf 100644 --- a/pypiserver/_app.py +++ b/pypiserver/_app.py @@ -12,12 +12,12 @@ try: except ImportError: from StringIO import StringIO as BytesIO -if sys.version_info >= (3, 0): +try: #PY3 from urllib.parse import urljoin -else: +except ImportError: #PY2 from urlparse import urljoin -from bottle import static_file, redirect, request, response, HTTPError, Bottle +from bottle import static_file, redirect, request, response, HTTPError, Bottle, template from pypiserver import __version__ from pypiserver.core import listdir, find_packages, store, get_prefixes, exists @@ -135,21 +135,21 @@ def configure(root=None, config.welcome_msg = dedent("""\ Welcome to pypiserver!

Welcome to pypiserver!

-

This is a PyPI compatible package index serving %(NUMPKGS)s packages.

+

This is a PyPI compatible package index serving {{NUMPKGS}} packages.

To use this server with pip, run the the following command:

-            pip install -i %(URL)ssimple/ PACKAGE [PACKAGE2...]
+            pip install -i {{URL}}simple/ PACKAGE [PACKAGE2...]
             

To use this server with easy_install, run the the following command:

-            easy_install -i %(URL)ssimple/ PACKAGE
+            easy_install -i {{URL}}simple/ PACKAGE
             

-

The complete list of all packages can be found here or via the simple index.

+

The complete list of all packages can be found here or via the simple index.

-

This instance is running version %(VERSION)s of the pypiserver software.

+

This instance is running version {{VERSION}} of the pypiserver software.

\ """) @@ -194,7 +194,8 @@ def root(): except: numpkgs = 0 - return config.welcome_msg % dict( + msg = config.welcome_msg + '\n' ## Enrure template() does not considere `msg` as filename! + return template(msg, URL=request.url, VERSION=__version__, NUMPKGS=numpkgs, diff --git a/pypiserver/welcome.html b/pypiserver/welcome.html index ac3fc99..18e426f 100644 --- a/pypiserver/welcome.html +++ b/pypiserver/welcome.html @@ -1,18 +1,18 @@ Welcome to pypiserver!

Welcome to pypiserver!

-

This is a PyPI compatible package index serving %(NUMPKGS)s packages.

+

This is a PyPI compatible package index serving {{NUMPKGS}} packages.

To use this server with pip, run the the following command:

-pip install -i %(URL)ssimple/ PACKAGE [PACKAGE2...]
+pip install -i {{URL}}simple/ PACKAGE [PACKAGE2...]
 

To use this server with easy_install, run the the following command:

-easy_install -i %(URL)ssimple/ PACKAGE
+easy_install -i {{URL}}simple/ PACKAGE
 

-

The complete list of all packages can be found here or via the simple index.

+

The complete list of all packages can be found here or via the simple index.

-

This instance is running version %(VERSION)s of the pypiserver software.

+

This instance is running version {{VERSION}} of the pypiserver software.

diff --git a/tests/sample_msg.html b/tests/sample_msg.html index 6dbbef4..2c91f02 100644 --- a/tests/sample_msg.html +++ b/tests/sample_msg.html @@ -1,7 +1,7 @@ Hello pypiserver tester! -%(URL)s -%(VERSION)s -%(NUMPKGS)s -%(PACKAGES)s -%(SIMPLE)s +{{URL}} +{{VERSION}} +{{NUMPKGS}} +{{PACKAGES}} +{{SIMPLE}} diff --git a/tests/test_app.py b/tests/test_app.py index 69fe912..efbabe5 100755 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -56,7 +56,7 @@ def test_root_hostname(testapp): def test_root_welcome_msg(root): - wmsg = "Hey there!" + wmsg = "Hey there!" wfile = root.join("testwelcome.html") wfile.write(wmsg) @@ -66,6 +66,9 @@ def test_root_welcome_msg(root): resp = testapp.get("/") resp.mustcontain(wmsg) +def test_root_welcome_msg_antiXSS(testapp): + resp = testapp.get("/?Red", headers={"Host": "somehost.org"}) + resp.mustcontain("alert", "somehost.org", no="") def test_packages_empty(testapp): resp = testapp.get("/packages") @@ -243,4 +246,4 @@ def test_cache_control_set(root): root.join("foo_bar-1.0.tar.gz").write("") resp = app_with_cache.get("/packages/foo_bar-1.0.tar.gz") assert "Cache-Control" in resp.headers - assert resp.headers["Cache-Control"] == 'public, max-age=%s' % AGE \ No newline at end of file + assert resp.headers["Cache-Control"] == 'public, max-age=%s' % AGE From 7cc36aee0c038f3fc07396f5ae02c2c7caf64199 Mon Sep 17 00:00:00 2001 From: ankostis on tokoti Date: Mon, 23 Feb 2015 01:45:35 +0100 Subject: [PATCH 3/4] Improve welcome-msg tests and add XSS for when removing packages (probably not needed). --- pypiserver/_app.py | 2 +- tests/test_app.py | 60 ++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 54 insertions(+), 8 deletions(-) mode change 100755 => 100644 tests/test_app.py diff --git a/pypiserver/_app.py b/pypiserver/_app.py index 52807bf..9b644ce 100644 --- a/pypiserver/_app.py +++ b/pypiserver/_app.py @@ -194,7 +194,7 @@ def root(): except: numpkgs = 0 - msg = config.welcome_msg + '\n' ## Enrure template() does not considere `msg` as filename! + msg = config.welcome_msg + '\n' ## Ensure template() does not consider `msg` as filename! return template(msg, URL=request.url, VERSION=__version__, diff --git a/tests/test_app.py b/tests/test_app.py old mode 100755 new mode 100644 index efbabe5..7be4e76 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -41,6 +41,32 @@ def testpriv(priv): return webtest.TestApp(priv) +@pytest.fixture(params=[" ", ## Mustcontain test below fails when string is empty. + "Hey there!", + "Hey there!", + ]) +def welcome_file_no_vars(request, root): + wfile = root.join("testwelcome.html") + wfile.write_text(request.param, 'utf-8') + + return wfile + + +@pytest.fixture() +def welcome_file_all_vars(request, root): + msg =""" + {{URL}} + {{VERSION}} + {{NUMPKGS}} + {{PACKAGES}} + {{SIMPLE}} + """ + wfile = root.join("testwelcome.html") + wfile.write_text(msg, 'utf-8') + + return wfile + + def test_root_count(root, testapp): resp = testapp.get("/") resp.mustcontain("PyPI compatible package index serving 0 packages") @@ -55,21 +81,41 @@ def test_root_hostname(testapp): # go("http://systemexit.de/") -def test_root_welcome_msg(root): - wmsg = "Hey there!" - wfile = root.join("testwelcome.html") - wfile.write(wmsg) - +def test_root_welcome_msg_no_vars(root, welcome_file_no_vars): from pypiserver import app - app = app(root=root.strpath, welcome_file=wfile.strpath) + app = app(root=root.strpath, welcome_file=welcome_file_no_vars.strpath) testapp = webtest.TestApp(app) resp = testapp.get("/") - resp.mustcontain(wmsg) + from pypiserver import __version__ as pver + resp.mustcontain(welcome_file_no_vars.read_text('utf-8'), no=pver) + + +def test_root_welcome_msg_all_vars(root, welcome_file_all_vars): + from pypiserver import app + app = app(root=root.strpath, welcome_file=welcome_file_all_vars.strpath) + testapp = webtest.TestApp(app) + resp = testapp.get("/") + + from pypiserver import __version__ as pver + resp.mustcontain(pver) + def test_root_welcome_msg_antiXSS(testapp): + """https://github.com/pypiserver/pypiserver/issues/77""" resp = testapp.get("/?Red", headers={"Host": "somehost.org"}) resp.mustcontain("alert", "somehost.org", no="") + +def test_root_remove_not_found_msg_antiXSS(testapp): + """https://github.com/pypiserver/pypiserver/issues/77""" + resp = testapp.post("/", expect_errors=True, + headers={"Host": "somehost.org"}, + params={':action': 'remove_pkg', + 'name': 'Red', + 'version':'1.1.1'}) + resp.mustcontain("alert", "somehost.org", no="") + + def test_packages_empty(testapp): resp = testapp.get("/packages") assert len(resp.html("a")) == 0 From 10f42e829c7ebe657e21c7a0e5c0656ee39e5bf4 Mon Sep 17 00:00:00 2001 From: ankostis on tokoti Date: Mon, 23 Feb 2015 02:19:58 +0100 Subject: [PATCH 4/4] xss: Generate all index-listings with SimpleTemplate instead of string-substs (#see 77). - Add titles

in all index-listings. - FIX unicode errors on new TC's of prev commit. --- README.rst | 2 ++ pypiserver/_app.py | 64 +++++++++++++++++++++++++++++++--------------- tests/test_app.py | 6 ++--- 3 files changed, 48 insertions(+), 24 deletions(-) diff --git a/README.rst b/README.rst index f7ffa6a..38181a1 100644 --- a/README.rst +++ b/README.rst @@ -489,6 +489,8 @@ Changelog - #56, #70: Ignore non-packages when serving. - #58, #62: Log all http-requests. - #61: Possible to change welcome-msg. +- #77, #78: Avoid XSS by generating web-content with SimpleTemplate + instead of python's string-substs. 1.1.6 (2014-03-05) ------------------ diff --git a/pypiserver/_app.py b/pypiserver/_app.py index 9b644ce..c42a446 100644 --- a/pypiserver/_app.py +++ b/pypiserver/_app.py @@ -270,11 +270,20 @@ def simpleindex_redirect(): @app.route("/simple/") @auth("list") def simpleindex(): - res = ["Simple Index\n"] - for x in sorted(get_prefixes(packages())): - res.append('%s
\n' % (x, x)) - res.append("") - return "".join(res) + links = sorted(get_prefixes(packages())) + tmpl = """\ + + + Simple Index + + +

Simple Index

+ % for p in links: + {{p}}
+ + + """ + return template(tmpl, links=links) @app.route("/simple/:prefix") @@ -286,19 +295,25 @@ def simple(prefix=""): fp += "/" files = [x.relfn for x in sorted(find_packages(packages(), prefix=prefix), key=lambda x: x.parsed_version)] - if not files: if config.redirect_to_fallback: return redirect("%s/%s/" % (config.fallback_url.rstrip("/"), prefix)) return HTTPError(404) - res = ["Links for %s\n" % prefix, - "

Links for %s

\n" % prefix] - for x in files: - abspath = urljoin(fp, "../../packages/%s" % x.replace("\\", "/")) - - res.append('%s
\n' % (abspath, os.path.basename(x))) - res.append("\n") - return "".join(res) + + links = [(os.path.basename(f), urljoin(fp, "../../packages/%s" % f.replace("\\", "/"))) for f in files] + tmpl = """\ + + + Links for {{prefix}} + + +

Links for {{prefix}}

+ % for file, href in links: + {{file}}
+ + + """ + return template(tmpl, prefix=prefix, links=links) @app.route('/packages') @@ -311,13 +326,20 @@ def list_packages(): files = [x.relfn for x in sorted(find_packages(packages()), key=lambda x: (os.path.dirname(x.relfn), x.pkgname, x.parsed_version))] - - res = ["Index of packages\n"] - for x in files: - x = x.replace("\\", "/") - res.append('%s
\n' % (urljoin(fp, x), x)) - res.append("\n") - return "".join(res) + links = [(f.replace("\\", "/"), urljoin(fp, f)) for f in files] + tmpl = """\ + + + Index of packages + + +

Index of packages

+ % for file, href in links: + {{file}}
+ + + """ + return template(tmpl, links=links) @app.route('/packages/:filename#.*#') diff --git a/tests/test_app.py b/tests/test_app.py index 7be4e76..f08c2b7 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -47,7 +47,7 @@ def testpriv(priv): ]) def welcome_file_no_vars(request, root): wfile = root.join("testwelcome.html") - wfile.write_text(request.param, 'utf-8') + wfile.write(request.param) return wfile @@ -62,7 +62,7 @@ def welcome_file_all_vars(request, root): {{SIMPLE}} """ wfile = root.join("testwelcome.html") - wfile.write_text(msg, 'utf-8') + wfile.write(msg) return wfile @@ -87,7 +87,7 @@ def test_root_welcome_msg_no_vars(root, welcome_file_no_vars): testapp = webtest.TestApp(app) resp = testapp.get("/") from pypiserver import __version__ as pver - resp.mustcontain(welcome_file_no_vars.read_text('utf-8'), no=pver) + resp.mustcontain(welcome_file_no_vars.read(), no=pver) def test_root_welcome_msg_all_vars(root, welcome_file_all_vars):