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):