mirror of
https://github.com/pypiserver/pypiserver
synced 2024-11-09 16:45:51 +01:00
Merge pull request #78 from ankostis/antixss
FIX #77 XSS by generating http-responses with Bottle's SimpleTemplate
This commit is contained in:
commit
744282d5c2
@ -489,6 +489,8 @@ Changelog
|
|||||||
- #56, #70: Ignore non-packages when serving.
|
- #56, #70: Ignore non-packages when serving.
|
||||||
- #58, #62: Log all http-requests.
|
- #58, #62: Log all http-requests.
|
||||||
- #61: Possible to change welcome-msg.
|
- #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)
|
1.1.6 (2014-03-05)
|
||||||
------------------
|
------------------
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
__version_info__ = (1, 1, 7, 'beta.0')
|
__version_info__ = (1, 1, 7, 'beta.1')
|
||||||
version = __version__ = "1.1.7-beta.0"
|
version = __version__ = "1.1.7-beta.1"
|
||||||
|
|
||||||
|
|
||||||
def app(root=None,
|
def app(root=None,
|
||||||
|
@ -12,12 +12,12 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
from StringIO import StringIO as BytesIO
|
from StringIO import StringIO as BytesIO
|
||||||
|
|
||||||
if sys.version_info >= (3, 0):
|
try: #PY3
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
else:
|
except ImportError: #PY2
|
||||||
from urlparse import urljoin
|
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 import __version__
|
||||||
from pypiserver.core import listdir, find_packages, store, get_prefixes, exists
|
from pypiserver.core import listdir, find_packages, store, get_prefixes, exists
|
||||||
|
|
||||||
@ -135,21 +135,21 @@ def configure(root=None,
|
|||||||
config.welcome_msg = dedent("""\
|
config.welcome_msg = dedent("""\
|
||||||
<html><head><title>Welcome to pypiserver!</title></head><body>
|
<html><head><title>Welcome to pypiserver!</title></head><body>
|
||||||
<h1>Welcome to pypiserver!</h1>
|
<h1>Welcome to pypiserver!</h1>
|
||||||
<p>This is a PyPI compatible package index serving %(NUMPKGS)s packages.</p>
|
<p>This is a PyPI compatible package index serving {{NUMPKGS}} packages.</p>
|
||||||
|
|
||||||
<p> To use this server with pip, run the the following command:
|
<p> To use this server with pip, run the the following command:
|
||||||
<blockquote><pre>
|
<blockquote><pre>
|
||||||
pip install -i %(URL)ssimple/ PACKAGE [PACKAGE2...]
|
pip install -i {{URL}}simple/ PACKAGE [PACKAGE2...]
|
||||||
</pre></blockquote></p>
|
</pre></blockquote></p>
|
||||||
|
|
||||||
<p> To use this server with easy_install, run the the following command:
|
<p> To use this server with easy_install, run the the following command:
|
||||||
<blockquote><pre>
|
<blockquote><pre>
|
||||||
easy_install -i %(URL)ssimple/ PACKAGE
|
easy_install -i {{URL}}simple/ PACKAGE
|
||||||
</pre></blockquote></p>
|
</pre></blockquote></p>
|
||||||
|
|
||||||
<p>The complete list of all packages can be found <a href="%(PACKAGES)s">here</a> or via the <a href="%(SIMPLE)s">simple</a> index.</p>
|
<p>The complete list of all packages can be found <a href="{{PACKAGES}}">here</a> or via the <a href="{{SIMPLE}}">simple</a> index.</p>
|
||||||
|
|
||||||
<p>This instance is running version %(VERSION)s of the <a href="http://pypi.python.org/pypi/pypiserver">pypiserver</a> software.</p>
|
<p>This instance is running version {{VERSION}} of the <a href="http://pypi.python.org/pypi/pypiserver">pypiserver</a> software.</p>
|
||||||
</body></html>\
|
</body></html>\
|
||||||
""")
|
""")
|
||||||
|
|
||||||
@ -194,7 +194,8 @@ def root():
|
|||||||
except:
|
except:
|
||||||
numpkgs = 0
|
numpkgs = 0
|
||||||
|
|
||||||
return config.welcome_msg % dict(
|
msg = config.welcome_msg + '\n' ## Ensure template() does not consider `msg` as filename!
|
||||||
|
return template(msg,
|
||||||
URL=request.url,
|
URL=request.url,
|
||||||
VERSION=__version__,
|
VERSION=__version__,
|
||||||
NUMPKGS=numpkgs,
|
NUMPKGS=numpkgs,
|
||||||
@ -269,11 +270,20 @@ def simpleindex_redirect():
|
|||||||
@app.route("/simple/")
|
@app.route("/simple/")
|
||||||
@auth("list")
|
@auth("list")
|
||||||
def simpleindex():
|
def simpleindex():
|
||||||
res = ["<html><head><title>Simple Index</title></head><body>\n"]
|
links = sorted(get_prefixes(packages()))
|
||||||
for x in sorted(get_prefixes(packages())):
|
tmpl = """\
|
||||||
res.append('<a href="%s/">%s</a><br>\n' % (x, x))
|
<html>
|
||||||
res.append("</body></html>")
|
<head>
|
||||||
return "".join(res)
|
<title>Simple Index</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Simple Index</h1>
|
||||||
|
% for p in links:
|
||||||
|
<a href="{{p}}/">{{p}}</a><br>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
return template(tmpl, links=links)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/simple/:prefix")
|
@app.route("/simple/:prefix")
|
||||||
@ -285,19 +295,25 @@ def simple(prefix=""):
|
|||||||
fp += "/"
|
fp += "/"
|
||||||
|
|
||||||
files = [x.relfn for x in sorted(find_packages(packages(), prefix=prefix), key=lambda x: x.parsed_version)]
|
files = [x.relfn for x in sorted(find_packages(packages(), prefix=prefix), key=lambda x: x.parsed_version)]
|
||||||
|
|
||||||
if not files:
|
if not files:
|
||||||
if config.redirect_to_fallback:
|
if config.redirect_to_fallback:
|
||||||
return redirect("%s/%s/" % (config.fallback_url.rstrip("/"), prefix))
|
return redirect("%s/%s/" % (config.fallback_url.rstrip("/"), prefix))
|
||||||
return HTTPError(404)
|
return HTTPError(404)
|
||||||
res = ["<html><head><title>Links for %s</title></head><body>\n" % prefix,
|
|
||||||
"<h1>Links for %s</h1>\n" % prefix]
|
|
||||||
for x in files:
|
|
||||||
abspath = urljoin(fp, "../../packages/%s" % x.replace("\\", "/"))
|
|
||||||
|
|
||||||
res.append('<a href="%s">%s</a><br>\n' % (abspath, os.path.basename(x)))
|
links = [(os.path.basename(f), urljoin(fp, "../../packages/%s" % f.replace("\\", "/"))) for f in files]
|
||||||
res.append("</body></html>\n")
|
tmpl = """\
|
||||||
return "".join(res)
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Links for {{prefix}}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Links for {{prefix}}</h1>
|
||||||
|
% for file, href in links:
|
||||||
|
<a href="{{href}}">{{file}}</a><br>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
return template(tmpl, prefix=prefix, links=links)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/packages')
|
@app.route('/packages')
|
||||||
@ -310,13 +326,20 @@ def list_packages():
|
|||||||
|
|
||||||
files = [x.relfn for x in sorted(find_packages(packages()),
|
files = [x.relfn for x in sorted(find_packages(packages()),
|
||||||
key=lambda x: (os.path.dirname(x.relfn), x.pkgname, x.parsed_version))]
|
key=lambda x: (os.path.dirname(x.relfn), x.pkgname, x.parsed_version))]
|
||||||
|
links = [(f.replace("\\", "/"), urljoin(fp, f)) for f in files]
|
||||||
res = ["<html><head><title>Index of packages</title></head><body>\n"]
|
tmpl = """\
|
||||||
for x in files:
|
<html>
|
||||||
x = x.replace("\\", "/")
|
<head>
|
||||||
res.append('<a href="%s">%s</a><br>\n' % (urljoin(fp, x), x))
|
<title>Index of packages</title>
|
||||||
res.append("</body></html>\n")
|
</head>
|
||||||
return "".join(res)
|
<body>
|
||||||
|
<h1>Index of packages</h1>
|
||||||
|
% for file, href in links:
|
||||||
|
<a href="{{href}}">{{file}}</a><br>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
return template(tmpl, links=links)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/packages/:filename#.*#')
|
@app.route('/packages/:filename#.*#')
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
<html><head><title>Welcome to pypiserver!</title></head><body>
|
<html><head><title>Welcome to pypiserver!</title></head><body>
|
||||||
<h1>Welcome to pypiserver!</h1>
|
<h1>Welcome to pypiserver!</h1>
|
||||||
<p>This is a PyPI compatible package index serving %(NUMPKGS)s packages.</p>
|
<p>This is a PyPI compatible package index serving {{NUMPKGS}} packages.</p>
|
||||||
|
|
||||||
<p> To use this server with pip, run the the following command:
|
<p> To use this server with pip, run the the following command:
|
||||||
<blockquote><pre>
|
<blockquote><pre>
|
||||||
pip install -i %(URL)ssimple/ PACKAGE [PACKAGE2...]
|
pip install -i {{URL}}simple/ PACKAGE [PACKAGE2...]
|
||||||
</pre></blockquote></p>
|
</pre></blockquote></p>
|
||||||
|
|
||||||
<p> To use this server with easy_install, run the the following command:
|
<p> To use this server with easy_install, run the the following command:
|
||||||
<blockquote><pre>
|
<blockquote><pre>
|
||||||
easy_install -i %(URL)ssimple/ PACKAGE
|
easy_install -i {{URL}}simple/ PACKAGE
|
||||||
</pre></blockquote></p>
|
</pre></blockquote></p>
|
||||||
|
|
||||||
<p>The complete list of all packages can be found <a href="%(PACKAGES)s">here</a> or via the <a href="%(SIMPLE)s">simple</a> index.</p>
|
<p>The complete list of all packages can be found <a href="{{PACKAGES}}">here</a> or via the <a href="{{SIMPLE}}">simple</a> index.</p>
|
||||||
|
|
||||||
<p>This instance is running version %(VERSION)s of the <a href="http://pypi.python.org/pypi/pypiserver">pypiserver</a> software.</p>
|
<p>This instance is running version {{VERSION}} of the <a href="http://pypi.python.org/pypi/pypiserver">pypiserver</a> software.</p>
|
||||||
</body></html>
|
</body></html>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
Hello pypiserver tester!
|
Hello pypiserver tester!
|
||||||
%(URL)s
|
{{URL}}
|
||||||
%(VERSION)s
|
{{VERSION}}
|
||||||
%(NUMPKGS)s
|
{{NUMPKGS}}
|
||||||
%(PACKAGES)s
|
{{PACKAGES}}
|
||||||
%(SIMPLE)s
|
{{SIMPLE}}
|
||||||
|
|
||||||
|
63
tests/test_app.py
Executable file → Normal file
63
tests/test_app.py
Executable file → Normal file
@ -41,6 +41,32 @@ def testpriv(priv):
|
|||||||
return webtest.TestApp(priv)
|
return webtest.TestApp(priv)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(params=[" ", ## Mustcontain test below fails when string is empty.
|
||||||
|
"Hey there!",
|
||||||
|
"<html><body>Hey there!</body></html>",
|
||||||
|
])
|
||||||
|
def welcome_file_no_vars(request, root):
|
||||||
|
wfile = root.join("testwelcome.html")
|
||||||
|
wfile.write(request.param)
|
||||||
|
|
||||||
|
return wfile
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def welcome_file_all_vars(request, root):
|
||||||
|
msg ="""
|
||||||
|
{{URL}}
|
||||||
|
{{VERSION}}
|
||||||
|
{{NUMPKGS}}
|
||||||
|
{{PACKAGES}}
|
||||||
|
{{SIMPLE}}
|
||||||
|
"""
|
||||||
|
wfile = root.join("testwelcome.html")
|
||||||
|
wfile.write(msg)
|
||||||
|
|
||||||
|
return wfile
|
||||||
|
|
||||||
|
|
||||||
def test_root_count(root, testapp):
|
def test_root_count(root, testapp):
|
||||||
resp = testapp.get("/")
|
resp = testapp.get("/")
|
||||||
resp.mustcontain("PyPI compatible package index serving 0 packages")
|
resp.mustcontain("PyPI compatible package index serving 0 packages")
|
||||||
@ -55,16 +81,39 @@ def test_root_hostname(testapp):
|
|||||||
# go("http://systemexit.de/")
|
# go("http://systemexit.de/")
|
||||||
|
|
||||||
|
|
||||||
def test_root_welcome_msg(root):
|
def test_root_welcome_msg_no_vars(root, welcome_file_no_vars):
|
||||||
wmsg = "Hey there!"
|
|
||||||
wfile = root.join("testwelcome.html")
|
|
||||||
wfile.write(wmsg)
|
|
||||||
|
|
||||||
from pypiserver import app
|
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)
|
testapp = webtest.TestApp(app)
|
||||||
resp = testapp.get("/")
|
resp = testapp.get("/")
|
||||||
resp.mustcontain(wmsg)
|
from pypiserver import __version__ as pver
|
||||||
|
resp.mustcontain(welcome_file_no_vars.read(), 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("/?<alert>Red</alert>", headers={"Host": "somehost.org"})
|
||||||
|
resp.mustcontain("alert", "somehost.org", no="<alert>")
|
||||||
|
|
||||||
|
|
||||||
|
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': '<alert>Red</alert>',
|
||||||
|
'version':'1.1.1'})
|
||||||
|
resp.mustcontain("alert", "somehost.org", no="<alert>")
|
||||||
|
|
||||||
|
|
||||||
def test_packages_empty(testapp):
|
def test_packages_empty(testapp):
|
||||||
|
Loading…
Reference in New Issue
Block a user