From cdb99a1f1a57c6de5d07c2be952253e2600f00a4 Mon Sep 17 00:00:00 2001 From: Jimmy Berry Date: Mon, 25 Mar 2019 17:46:14 -0500 Subject: [PATCH 1/3] obs_operator: calculate apiurl from host instead of origin header. Debated originally, but was attempting to allow operator to run on different domain from request origin and handle multiple origins. This does not work in practice since openSUSE and SUSE https certs are not present on same machine. As such, host works better since it allows for non-cross-origin requests to work without having to specify an apiurl in startup arguments. --- obs_operator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/obs_operator.py b/obs_operator.py index 7fce8ca0..1604d284 100755 --- a/obs_operator.py +++ b/obs_operator.py @@ -98,12 +98,12 @@ class RequestHandler(BaseHTTPRequestHandler): if self.apiurl: return self.apiurl - origin = self.headers.get('Origin') - if not origin: + host = self.headers.get('Host') + if not host: return None # Strip port if present. - domain = urlparse(origin).netloc.split(':', 2)[0] + domain = host.split(':', 2)[0] if '.' not in domain: return None From d3ff38cbf8edd16b1b28cd72fbffd1998a99f643 Mon Sep 17 00:00:00 2001 From: Jimmy Berry Date: Mon, 25 Mar 2019 17:47:07 -0500 Subject: [PATCH 2/3] obs_operator: raise exceptions when osc request environment cannot be aquired. --- obs_operator.py | 44 +++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/obs_operator.py b/obs_operator.py index 1604d284..41380ed6 100755 --- a/obs_operator.py +++ b/obs_operator.py @@ -55,13 +55,16 @@ class RequestHandler(BaseHTTPRequestHandler): self.end_headers() return - with OSCRequestEnvironment(self) as oscrc_file: - func = getattr(self, 'handle_{}'.format(path_prefix.replace('/', '_'))) - command = func(path_parts[2:], query) + try: + with OSCRequestEnvironment(self) as oscrc_file: + func = getattr(self, 'handle_{}'.format(path_prefix.replace('/', '_'))) + command = func(path_parts[2:], query) - self.end_headers() - if command and not self.execute(oscrc_file, command): - self.write_string('failed') + self.end_headers() + if command and not self.execute(oscrc_file, command): + self.write_string('failed') + except OSCRequestEnvironmentException as e: + self.write_string(str(e)) def do_POST(self): action = self.path.lstrip('/') @@ -79,16 +82,19 @@ class RequestHandler(BaseHTTPRequestHandler): if self.debug: print('data: {}'.format(data)) - with OSCRequestEnvironment(self, user) as oscrc_file: - func = getattr(self, 'handle_{}'.format(action)) - commands = func(data) - self.end_headers() + try: + with OSCRequestEnvironment(self, user) as oscrc_file: + func = getattr(self, 'handle_{}'.format(action)) + commands = func(data) + self.end_headers() - for command in commands: - self.write_string('$ {}\n'.format(' '.join(command))) - if not self.execute(oscrc_file, command): - self.write_string('failed') - break + for command in commands: + self.write_string('$ {}\n'.format(' '.join(command))) + if not self.execute(oscrc_file, command): + self.write_string('failed') + break + except OSCRequestEnvironmentException as e: + self.write_string(str(e)) def data_parse(self): data = self.rfile.read(int(self.headers['Content-Length'])) @@ -213,13 +219,14 @@ class OSCRequestEnvironment(object): if not apiurl: self.handler.send_response(400) self.handler.end_headers() - return + raise OSCRequestEnvironmentException('unable to determine apiurl') session = self.handler.session_get() if not session: self.handler.send_response(401) self.handler.end_headers() - return + raise OSCRequestEnvironmentException('unable to determine session') + if self.handler.debug: print('apiurl: {}'.format(apiurl)) print('session: {}'.format(session)) @@ -244,6 +251,9 @@ class OSCRequestEnvironment(object): self.cookiejar_file.__exit__(exc_type, exc_val, exc_tb) self.oscrc_file.__exit__(exc_type, exc_val, exc_tb) +class OSCRequestEnvironmentException(Exception): + pass + def main(args): RequestHandler.apiurl = args.apiurl RequestHandler.session = args.session From 2a3def39fd589be32652ba63b339d378a144e753 Mon Sep 17 00:00:00 2001 From: Jimmy Berry Date: Tue, 26 Mar 2019 18:17:21 -0500 Subject: [PATCH 3/3] obs_operator: verify that origin root-domain matches host domain. Reduces the attack surface by limiting sites that can initiate a cross-domain request to sub-domains of the domain on which the operator server is running. --- obs_operator.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/obs_operator.py b/obs_operator.py index 41380ed6..f2b37013 100755 --- a/obs_operator.py +++ b/obs_operator.py @@ -117,6 +117,16 @@ class RequestHandler(BaseHTTPRequestHandler): domain_parent = '.'.join(domain.split('.')[1:]) return 'https://api.{}'.format(domain_parent) + def origin_domain_get(self): + origin = self.headers.get('Origin') + if origin is not None: + # Strip port if present. + domain = urlparse(origin).netloc.split(':', 2)[0] + if '.' in domain: + return '.'.join(domain.split('.')[1:]) + + return None + def session_get(self): if self.session: return self.session @@ -216,10 +226,14 @@ class OSCRequestEnvironment(object): def __enter__(self): apiurl = self.handler.apiurl_get() - if not apiurl: + origin_domain = self.handler.origin_domain_get() + if not apiurl or (origin_domain and not apiurl.endswith(origin_domain)): self.handler.send_response(400) self.handler.end_headers() - raise OSCRequestEnvironmentException('unable to determine apiurl') + if not apiurl: + raise OSCRequestEnvironmentException('unable to determine apiurl') + else: + raise OSCRequestEnvironmentException('origin does not match host domain') session = self.handler.session_get() if not session: @@ -233,8 +247,9 @@ class OSCRequestEnvironment(object): self.handler.send_response(200) self.handler.send_header('Content-type', 'text/plain') - self.handler.send_header('Access-Control-Allow-Credentials', 'true') - self.handler.send_header('Access-Control-Allow-Origin', self.handler.headers.get('Origin')) + if origin_domain: + self.handler.send_header('Access-Control-Allow-Credentials', 'true') + self.handler.send_header('Access-Control-Allow-Origin', self.handler.headers.get('Origin')) self.cookiejar_file = tempfile.NamedTemporaryFile() self.oscrc_file = tempfile.NamedTemporaryFile()