Authenticate connections to web_config service

- Require all requests to use a session path.
 - Use a redirect file to avoid exposing the '/start' URL on the
   command line, as it contains the cookie value.

Fix for CVE-2014-2914.
Closes #1438.
This commit is contained in:
David Adam 2014-08-04 13:34:26 +08:00
parent 8844f0c142
commit 4ae2753025
2 changed files with 74 additions and 15 deletions

View File

@ -83,7 +83,7 @@ controllers.controller("colorsController", function($scope, $http) {
$scope.getCurrentTheme = function() {
$http.get("/colors/").success(function(data, status, headers, config) {
$http.get("colors/").success(function(data, status, headers, config) {
var currentScheme = { "name": "Current", "colors":[], "preferred_background": "" };
for (var i in data) {
currentScheme[data[i].name] = data[i].color;
@ -103,7 +103,7 @@ controllers.controller("colorsController", function($scope, $http) {
var remaining = settingNames.length;
for (name in settingNames) {
var postData = "what=" + settingNames[name] + "&color=" + $scope.selectedColorScheme[settingNames[name]] + "&background_color=&bold=&underline=";
$http.post("/set_color/", postData, { headers: {'Content-Type': 'application/x-www-form-urlencoded'} }).success(function(data, status, headers, config) {
$http.post("set_color/", postData, { headers: {'Content-Type': 'application/x-www-form-urlencoded'} }).success(function(data, status, headers, config) {
if (status == 200) {
remaining -= 1;
if (remaining == 0) {
@ -124,7 +124,7 @@ controllers.controller("promptController", function($scope, $http) {
$scope.savePromptButtonTitle = "Set Prompt";
$scope.fetchSamplePrompts= function() {
$http.get("/sample_prompts/").success(function(data, status, headers, config) {
$http.get("sample_prompts/").success(function(data, status, headers, config) {
$scope.samplePrompts = data;
$scope.samplePromptsArrayArray = get_colors_as_nested_array($scope.samplePrompts, 1);
@ -140,7 +140,7 @@ controllers.controller("promptController", function($scope, $http) {
}
$scope.setNewPrompt = function(selectedPrompt) {
$http.post("/set_prompt/","what=" + encodeURIComponent(selectedPrompt.function), { headers: {'Content-Type': 'application/x-www-form-urlencoded'} }).success(function(data, status, headers, config){
$http.post("set_prompt/","what=" + encodeURIComponent(selectedPrompt.function), { headers: {'Content-Type': 'application/x-www-form-urlencoded'} }).success(function(data, status, headers, config){
// Update attributes of current prompt and select it
$scope.samplePrompts[0].demo = selectedPrompt.demo;
@ -171,7 +171,7 @@ controllers.controller("functionsController", function($scope, $http) {
}
$scope.fetchFunctions= function() {
$http.get("/functions/").success(function(data, status, headers, config) {
$http.get("functions/").success(function(data, status, headers, config) {
$scope.functions = data;
$scope.selectFunction($scope.functions[0]);
})};
@ -195,7 +195,7 @@ controllers.controller("functionsController", function($scope, $http) {
}
$scope.fetchFunctionDefinition = function(name) {
$http.post("/get_function/","what=" + name, { headers: {'Content-Type': 'application/x-www-form-urlencoded'} }).success(function(data, status, headers, config) {
$http.post("get_function/","what=" + name, { headers: {'Content-Type': 'application/x-www-form-urlencoded'} }).success(function(data, status, headers, config) {
$scope.functionDefinition = $scope.cleanupFishFunction(data[0]);
})};
@ -206,7 +206,7 @@ controllers.controller("variablesController", function($scope, $http) {
$scope.query = null;
$scope.fetchVariables= function() {
$http.get("/variables/").success(function(data, status, headers, config) {
$http.get("variables/").success(function(data, status, headers, config) {
$scope.variables = data;
})};
@ -247,7 +247,7 @@ controllers.controller("historyController", function($scope, $http, $timeout) {
}
// Get history from server
$scope.fetchHistory = function() {
$http.get("/history/").success(function(data, status, headers, config) {
$http.get("history/").success(function(data, status, headers, config) {
$scope.historySize = data.length;
$scope.remainingItems = data;
@ -257,7 +257,7 @@ controllers.controller("historyController", function($scope, $http, $timeout) {
$scope.deleteHistoryItem = function(item) {
index = $scope.historyItems.indexOf(item);
$http.post("/delete_history_item/","what=" + encodeURIComponent(item), { headers: {'Content-Type': 'application/x-www-form-urlencoded'} }).success(function(data, status, headers, config) {
$http.post("delete_history_item/","what=" + encodeURIComponent(item), { headers: {'Content-Type': 'application/x-www-form-urlencoded'} }).success(function(data, status, headers, config) {
$scope.historyItems.splice(index, 1);
})};
@ -278,7 +278,7 @@ controllers.controller("historyController", function($scope, $http, $timeout) {
controllers.controller("bindingsController", function($scope, $http) {
$scope.bindings = [];
$scope.fetchBindings = function() {
$http.get("/bindings/").success(function(data, status, headers, config) {
$http.get("bindings/").success(function(data, status, headers, config) {
$scope.bindings = data;
})};

View File

@ -26,7 +26,7 @@ if term:
os.environ['TERM'] = term
import subprocess
import re, socket, cgi, select, time, glob
import re, socket, cgi, select, time, glob, random, string
try:
import json
except ImportError:
@ -693,9 +693,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/':
@ -727,6 +734,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
@ -788,6 +803,18 @@ class FishConfigHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
""" Disable request logging """
pass
redirect_template_html = """
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="refresh" content="0;URL='%s'" />
</head>
<body>
<p><a href="%s">Start the Fish Web config</a></p>
</body>
</html>
"""
# find fish
fish_bin_dir = os.environ.get('__fish_bin_dir')
fish_bin_path = None
@ -823,6 +850,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:
@ -852,9 +882,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()
@ -871,3 +928,5 @@ try:
except KeyboardInterrupt:
print("\nShutting down.")
# Clean up temporary file
os.remove(filename)