Skip to content

Commit 3427c25

Browse files
committed
Escape unsafe query string when rendering to html
1 parent 9ae3b95 commit 3427c25

File tree

2 files changed

+39
-7
lines changed

2 files changed

+39
-7
lines changed

oauth2cli/authcode.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
try: # Python 3
1616
from http.server import HTTPServer, BaseHTTPRequestHandler
1717
from urllib.parse import urlparse, parse_qs, urlencode
18+
from html import escape
1819
except ImportError: # Fall back to Python 2
1920
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
2021
from urlparse import urlparse, parse_qs
2122
from urllib import urlencode
23+
from cgi import escape
2224

2325

2426
logger = logging.getLogger(__name__)
@@ -77,25 +79,37 @@ def _qs2kv(qs):
7779
for k, v in qs.items()}
7880

7981

82+
def _is_html(text):
83+
return text.startswith("<") # Good enough for our purpose
84+
85+
86+
def _escape(key_value_pairs):
87+
return {k: escape(v) for k, v in key_value_pairs.items()}
88+
89+
8090
class _AuthCodeHandler(BaseHTTPRequestHandler):
8191
def do_GET(self):
8292
# For flexibility, we choose to not check self.path matching redirect_uri
8393
#assert self.path.startswith('/THE_PATH_REGISTERED_BY_THE_APP')
8494
qs = parse_qs(urlparse(self.path).query)
8595
if qs.get('code') or qs.get("error"): # So, it is an auth response
86-
self.server.auth_response = _qs2kv(qs)
87-
logger.debug("Got auth response: %s", self.server.auth_response)
96+
auth_response = _qs2kv(qs)
97+
logger.debug("Got auth response: %s", auth_response)
8898
template = (self.server.success_template
8999
if "code" in qs else self.server.error_template)
90-
self._send_full_response(
91-
template.safe_substitute(**self.server.auth_response))
100+
if _is_html(template.template):
101+
safe_data = _escape(auth_response)
102+
else:
103+
safe_data = auth_response
104+
self._send_full_response(template.safe_substitute(**safe_data))
105+
self.server.auth_response = auth_response # Set it now, after the response is likely sent
92106
# NOTE: Don't do self.server.shutdown() here. It'll halt the server.
93107
else:
94108
self._send_full_response(self.server.welcome_page)
95109

96110
def _send_full_response(self, body, is_ok=True):
97111
self.send_response(200 if is_ok else 400)
98-
content_type = 'text/html' if body.startswith('<') else 'text/plain'
112+
content_type = 'text/html' if _is_html(body) else 'text/plain'
99113
self.send_header('Content-type', content_type)
100114
self.end_headers()
101115
self.wfile.write(body.encode("utf-8"))
@@ -318,6 +332,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
318332
default="https://login.microsoftonline.com/common/oauth2/v2.0/authorize")
319333
p.add_argument('client_id', help="The client_id of your application")
320334
p.add_argument('--port', type=int, default=0, help="The port in redirect_uri")
335+
p.add_argument('--timeout', type=int, default=60, help="Timeout value, in second")
321336
p.add_argument('--host', default="127.0.0.1", help="The host of redirect_uri")
322337
p.add_argument('--scope', default=None, help="The scope list")
323338
args = parser.parse_args()
@@ -331,8 +346,8 @@ def __exit__(self, exc_type, exc_val, exc_tb):
331346
auth_uri=flow["auth_uri"],
332347
welcome_template=
333348
"<a href='$auth_uri'>Sign In</a>, or <a href='$abort_uri'>Abort</a",
334-
error_template="Oh no. $error",
349+
error_template="<html>Oh no. $error</html>",
335350
success_template="Oh yeah. Got $code",
336-
timeout=60,
351+
timeout=args.timeout,
337352
state=flow["state"], # Optional
338353
), indent=4))

tests/test_authcode.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import socket
33
import sys
44

5+
import requests
6+
57
from oauth2cli.authcode import AuthCodeReceiver
68

79

@@ -23,3 +25,18 @@ def test_no_two_concurrent_receivers_can_listen_on_same_port(self):
2325
with AuthCodeReceiver(port=receiver.get_port()):
2426
pass
2527

28+
def test_template_should_escape_input(self):
29+
with AuthCodeReceiver() as receiver:
30+
receiver._scheduled_actions = [( # Injection happens here when the port is known
31+
1, # Delay it until the receiver is activated by get_auth_response()
32+
lambda: self.assertEqual(
33+
"<html>&lt;tag&gt;foo&lt;/tag&gt;</html>",
34+
requests.get("http://localhost:{}?error=<tag>foo</tag>".format(
35+
receiver.get_port())).text,
36+
"Unsafe data in HTML should be escaped",
37+
))]
38+
receiver.get_auth_response( # Starts server and hang until timeout
39+
timeout=3,
40+
error_template="<html>$error</html>",
41+
)
42+

0 commit comments

Comments
 (0)