commit 397249a8d5a939d044da8ecfbb1654d48ce5a153 Author: David Adam Date: Mon Aug 4 13:34:26 2014 +0800 Authenticate connections to web_config service - Require all requests to use a session path. - Use a redirect file to avoid exposing the URL on the command line, as it contains the session path. Fix for CVE-2014-2914. Closes #1438. diff --git a/share/tools/web_config/index.html b/share/tools/web_config/index.html index 22cd470..90df114 100644 --- a/share/tools/web_config/index.html +++ b/share/tools/web_config/index.html @@ -556,7 +556,7 @@ function switch_tab(new_tab) { if (new_tab == 'tab_colors') { /* Keep track of whether this is the first element */ var first = true - run_get_request('/colors/', function(key_and_values){ + run_get_request('colors/', function(key_and_values){ /* Result is name, description, value */ var key = key_and_values[0] var description = key_and_values[1] @@ -577,7 +577,7 @@ function switch_tab(new_tab) { sample_prompts.length = 0 /* Color the first one blue */ var first = true; - run_get_request('/sample_prompts/', function(sample_prompt){ + run_get_request('sample_prompts/', function(sample_prompt){ var name = sample_prompt['name'] sample_prompts[name] = sample_prompt var color = first ? '66F' : 'AAA' @@ -594,7 +594,7 @@ function switch_tab(new_tab) { } else if (new_tab == 'tab_functions') { /* Keep track of whether this is the first element */ var first = true - run_get_request('/functions/', function(contents){ + run_get_request('functions/', function(contents){ var elem = create_master_element(contents, false/* description */, 'AAAAAA', '11pt', select_function_master_element) if (first) { /* It's the first element, so select it, so something gets selected */ @@ -606,7 +606,7 @@ function switch_tab(new_tab) { $('#master_detail_table').show() wants_data_table = false } else if (new_tab == 'tab_variables') { - run_get_request_with_bulk_handler('/variables/', function(json_contents){ + run_get_request_with_bulk_handler('variables/', function(json_contents){ var rows = new Array() for (var i = 0; i < json_contents.length; i++) { var contents = json_contents[i] @@ -622,7 +622,7 @@ function switch_tab(new_tab) { } else if (new_tab == 'tab_history') { // Clear the history map history_element_map.length = 0 - run_get_request_with_bulk_handler('/history/', function(json_contents){ + run_get_request_with_bulk_handler('history/', function(json_contents){ start = new Date().getTime() var rows = new Array() for (var i = 0; i < json_contents.length; i++) { @@ -757,7 +757,7 @@ function select_color_master_element(elem) { function select_function_master_element(elem) { select_master_element(elem) - run_post_request('/get_function/', { + run_post_request('get_function/', { what: current_master_element_name() }, function(contents){ /* Replace leading tabs and groups of four spaces at the beginning of a line with two spaces. */ @@ -773,7 +773,7 @@ function select_sample_prompt_master_element(elem) { select_master_element(elem) var name = current_master_element_name() sample_prompt = sample_prompts[name] - run_post_request('/get_sample_prompt/', { + run_post_request('get_sample_prompt/', { what: sample_prompt['function'] }, function(keys_and_values){ var prompt_func = keys_and_values['function'] @@ -788,7 +788,7 @@ function select_sample_prompt_master_element(elem) { function select_current_prompt_master_element(elem) { $('.prompt_save_button').hide() select_master_element(elem) - run_get_request_with_bulk_handler('/current_prompt/', function(keys_and_values){ + run_get_request_with_bulk_handler('current_prompt/', function(keys_and_values){ var prompt_func = keys_and_values['function'] var prompt_demo = keys_and_values['demo'] var prompt_font_size = keys_and_values['font_size'] @@ -801,7 +801,7 @@ function select_current_prompt_master_element(elem) { function save_current_prompt() { var name = current_master_element_name() var sample_prompt = sample_prompts[name] - run_post_request('/set_prompt/', { + run_post_request('set_prompt/', { what: sample_prompt['function'] }, function(contents){ if (contents == "OK") { @@ -817,7 +817,7 @@ function post_style_to_server() { if (! style) return - run_post_request('/set_color/', { + run_post_request('set_color/', { what: current_master_element_name(), color: style.color, background_color: style.background_color, @@ -1221,7 +1221,7 @@ function escape_HTML(foo) { function tell_fish_to_delete_element(idx) { var row_elem = $('#data_table_row_' + idx) var txt = history_element_map[idx] - run_post_request('/delete_history_item/', { + run_post_request('delete_history_item/', { what: txt }, function(contents){ if (contents == "OK") { diff --git a/share/tools/web_config/webconfig.py b/share/tools/web_config/webconfig.py index 1b9250b..2a103eb 100755 --- a/share/tools/web_config/webconfig.py +++ b/share/tools/web_config/webconfig.py @@ -17,7 +17,7 @@ else: from urllib.parse import parse_qs import webbrowser import subprocess -import re, socket, os, sys, cgi, select, time, glob +import re, socket, os, sys, cgi, select, time, glob, random, string try: import json except ImportError: @@ -485,9 +485,16 @@ class FishConfigHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): else: font_size = '18pt' return font_size - def do_GET(self): p = self.path + + authpath = '/' + authkey + if p.startswith(authpath): + p = p[len(authpath):] + else: + return self.send_error(403) + self.path = p + if p == '/colors/': output = self.do_get_colors() elif p == '/functions/': @@ -519,6 +526,14 @@ class FishConfigHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): def do_POST(self): p = self.path + + authpath = '/' + authkey + if p.startswith(authpath): + p = p[len(authpath):] + else: + return self.send_error(403) + self.path = p + if IS_PY2: ctype, pdict = cgi.parse_header(self.headers.getheader('content-type')) else: # Python 3 @@ -582,7 +597,19 @@ class FishConfigHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): def log_request(self, code='-', size='-'): """ Disable request logging """ pass - + +redirect_template_html = """ + + + + + + +

Start the Fish Web config

+ + +""" + # find fish fish_bin_dir = os.environ.get('__fish_bin_dir') fish_bin_path = None @@ -618,6 +645,9 @@ initial_wd = os.getcwd() where = os.path.dirname(sys.argv[0]) os.chdir(where) +# Generate a 16-byte random key as a hexadecimal string +authkey = hex(random.getrandbits(16*4))[2:] + # Try to find a suitable port PORT = 8000 while PORT <= 9000: @@ -647,9 +677,36 @@ if len(sys.argv) > 1: initial_tab = '#' + tab break -url = 'http://localhost:%d/%s' % (PORT, initial_tab) -print("Web config started at '%s'. Hit enter to stop." % url) -webbrowser.open(url) +url = 'http://localhost:%d/%s/%s' % (PORT, authkey, initial_tab) + +# Create temporary file to hold redirect to real server +# This prevents exposing the URL containing the authentication key on the command line +# (see CVE-2014-2914 or https://github.com/fish-shell/fish-shell/issues/1438) +if 'XDG_CACHE_HOME' in os.environ: + dirname = os.path.expanduser(os.path.expandvars('$XDG_CACHE_HOME/fish/')) +else: + dirname = os.path.expanduser('~/.cache/fish/') + +os.umask(0o0077) +try: + os.makedirs(dirname, 0o0700) +except OSError as e: + if e.errno == 17: + pass + else: + raise e + +randtoken = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(6)) +filename = dirname + 'web_config-%s.html' % randtoken + +f = open(filename, 'w') +f.write(redirect_template_html % (url, url)) +f.close() + +# Open temporary file as URL +fileurl = 'file://' + filename +print("Web config started at '%s'. Hit enter to stop." % fileurl) +webbrowser.open(fileurl) # Select on stdin and httpd stdin_no = sys.stdin.fileno() @@ -666,3 +723,5 @@ try: except KeyboardInterrupt: print("\nShutting down.") +# Clean up temporary file +os.remove(filename)