Added ENFORCE_BASEPATH option, fixes #68

This commit is contained in:
Daniel 2018-06-21 15:31:52 +00:00
parent ca4b91a094
commit 35ac89dcb1
5 changed files with 36 additions and 9 deletions

View file

@ -43,6 +43,8 @@ The IP address the service is listening on. By default it is binding to `0.0.0.0
The port the service is listening on. By default it is using 3218, but you can change this if you need to. The port the service is listening on. By default it is using 3218, but you can change this if you need to.
#### BASEPATH (string) #### BASEPATH (string)
It is possible to place configurator.py somewhere else. Set the `BASEPATH` to something like `"/home/homeassistant/.homeassistant"`, and no matter where you are running the configurator from, it will start serving files from there. This is needed if you plan on running the configurator with systemd. It is possible to place configurator.py somewhere else. Set the `BASEPATH` to something like `"/home/homeassistant/.homeassistant"`, and no matter where you are running the configurator from, it will start serving files from there. This is needed if you plan on running the configurator with systemd.
#### ENFORCE_BASEPATH (bool)
Set ENFORCE_BASEPATH to `True` to lock the configurator into the basepath and thereby prevent it from opening files outside of the BASEPATH
#### SSL_CERTIFICATE / SSL_KEY (string) #### SSL_CERTIFICATE / SSL_KEY (string)
If you're using SSL, set the paths to your SSL files here. This is similar to the SSL setup you can do in HASS. If you're using SSL, set the paths to your SSL files here. This is similar to the SSL setup you can do in HASS.
#### HASS_API (string) #### HASS_API (string)

View file

@ -1,6 +1,7 @@
Version 0.2.9 (2018-06-) Version 0.2.9 (2018-06-)
- Material Icons and HASS-help now open in new tab instead of modal (Issues #85 and #34) @danielperna84 - Material Icons and HASS-help now open in new tab instead of modal (Issues #85 and #34) @danielperna84
- Open file by URL (Issue #95) @danielperna84 - Open file by URL (Issue #95) @danielperna84
- Added ENFORCE_BASEPATH option (Issue #68) @danielperna84
Version 0.2.8 (2018-04-23) Version 0.2.8 (2018-04-23)
- Updated CDN libraries @jmart518 - Updated CDN libraries @jmart518

View file

@ -30,6 +30,9 @@ LISTENPORT = 3218
# Set BASEPATH to something like "/home/hass/.homeassistant/" if you're not # Set BASEPATH to something like "/home/hass/.homeassistant/" if you're not
# running the configurator from that path # running the configurator from that path
BASEPATH = None BASEPATH = None
# Set ENFORCE_BASEPATH to True to lock the configurator into the basepath and
# thereby prevent it from opening files outside of the BASEPATH
ENFORCE_BASEPATH = False
# Set the paths to a certificate and the key if you're using SSL, # Set the paths to a certificate and the key if you're using SSL,
# e.g "/etc/ssl/certs/mycert.pem" # e.g "/etc/ssl/certs/mycert.pem"
SSL_CERTIFICATE = None SSL_CERTIFICATE = None
@ -201,7 +204,7 @@ INDEX = Template(r"""<!DOCTYPE html>
color: #616161 !important; color: #616161 !important;
font-weight: 400; font-weight: 400;
display: inline-block; display: inline-block;
width: 185px; width: 182px;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
cursor: pointer; cursor: pointer;
@ -2387,7 +2390,10 @@ INDEX = Template(r"""<!DOCTYPE html>
function listdir(path) { function listdir(path) {
$.get(encodeURI("api/listdir?path=" + path), function(data) { $.get(encodeURI("api/listdir?path=" + path), function(data) {
renderpath(data); if (!data.error) {
renderpath(data);
}
console.log("Permission denied.");
}); });
document.getElementById("slide-out").scrollTop = 0; document.getElementById("slide-out").scrollTop = 0;
} }
@ -3314,7 +3320,7 @@ def signal_handler(sig, frame):
def load_settings(settingsfile): def load_settings(settingsfile):
global LISTENIP, LISTENPORT, BASEPATH, SSL_CERTIFICATE, SSL_KEY, HASS_API, \ global LISTENIP, LISTENPORT, BASEPATH, SSL_CERTIFICATE, SSL_KEY, HASS_API, \
HASS_API_PASSWORD, CREDENTIALS, ALLOWED_NETWORKS, BANNED_IPS, BANLIMIT, \ HASS_API_PASSWORD, CREDENTIALS, ALLOWED_NETWORKS, BANNED_IPS, BANLIMIT, \
DEV, IGNORE_PATTERN, DIRSFIRST, SESAME, VERIFY_HOSTNAME DEV, IGNORE_PATTERN, DIRSFIRST, SESAME, VERIFY_HOSTNAME, ENFORCE_BASEPATH
try: try:
if os.path.isfile(settingsfile): if os.path.isfile(settingsfile):
with open(settingsfile) as fptr: with open(settingsfile) as fptr:
@ -3322,6 +3328,7 @@ def load_settings(settingsfile):
LISTENIP = settings.get("LISTENIP", LISTENIP) LISTENIP = settings.get("LISTENIP", LISTENIP)
LISTENPORT = settings.get("LISTENPORT", LISTENPORT) LISTENPORT = settings.get("LISTENPORT", LISTENPORT)
BASEPATH = settings.get("BASEPATH", BASEPATH) BASEPATH = settings.get("BASEPATH", BASEPATH)
ENFORCE_BASEPATH = settings.get("ENFORCE_BASEPATH", ENFORCE_BASEPATH)
SSL_CERTIFICATE = settings.get("SSL_CERTIFICATE", SSL_CERTIFICATE) SSL_CERTIFICATE = settings.get("SSL_CERTIFICATE", SSL_CERTIFICATE)
SSL_KEY = settings.get("SSL_KEY", SSL_KEY) SSL_KEY = settings.get("SSL_KEY", SSL_KEY)
HASS_API = settings.get("HASS_API", HASS_API) HASS_API = settings.get("HASS_API", HASS_API)
@ -3340,6 +3347,12 @@ def load_settings(settingsfile):
LOG.warning("Not loading static settings") LOG.warning("Not loading static settings")
return False return False
def is_safe_path(basedir, path, follow_symlinks=True):
if follow_symlinks:
return os.path.realpath(path).startswith(basedir)
return os.path.abspath(path).startswith(basedir)
def get_dircontent(path, repo=None): def get_dircontent(path, repo=None):
dircontent = [] dircontent = []
if repo: if repo:
@ -3476,6 +3489,8 @@ class RequestHandler(BaseHTTPRequestHandler):
try: try:
if filename: if filename:
filename = unquote(filename[0]).encode('utf-8') filename = unquote(filename[0]).encode('utf-8')
if ENFORCE_BASEPATH and not is_safe_path(BASEPATH.encode('utf-8'), filename):
raise OSError('Access denied.')
if os.path.isfile(os.path.join(BASEDIR.encode('utf-8'), filename)): if os.path.isfile(os.path.join(BASEDIR.encode('utf-8'), filename)):
with open(os.path.join(BASEDIR.encode('utf-8'), filename)) as fptr: with open(os.path.join(BASEDIR.encode('utf-8'), filename)) as fptr:
content += fptr.read() content += fptr.read()
@ -3492,6 +3507,8 @@ class RequestHandler(BaseHTTPRequestHandler):
try: try:
if filename: if filename:
filename = unquote(filename[0]).encode('utf-8') filename = unquote(filename[0]).encode('utf-8')
if ENFORCE_BASEPATH and not is_safe_path(BASEPATH.encode('utf-8'), filename):
raise OSError('Access denied.')
LOG.info(filename) LOG.info(filename)
if os.path.isfile(os.path.join(BASEDIR.encode('utf-8'), filename)): if os.path.isfile(os.path.join(BASEDIR.encode('utf-8'), filename)):
with open(os.path.join(BASEDIR.encode('utf-8'), filename), 'rb') as fptr: with open(os.path.join(BASEDIR.encode('utf-8'), filename), 'rb') as fptr:
@ -3509,7 +3526,7 @@ class RequestHandler(BaseHTTPRequestHandler):
self.wfile.write(bytes(content, "utf8")) self.wfile.write(bytes(content, "utf8"))
return return
elif req.path.endswith('/api/listdir'): elif req.path.endswith('/api/listdir'):
content = "" content = {'error': None}
self.send_header('Content-type', 'text/json') self.send_header('Content-type', 'text/json')
self.end_headers() self.end_headers()
dirpath = query.get('path', None) dirpath = query.get('path', None)
@ -3517,6 +3534,8 @@ class RequestHandler(BaseHTTPRequestHandler):
if dirpath: if dirpath:
dirpath = unquote(dirpath[0]).encode('utf-8') dirpath = unquote(dirpath[0]).encode('utf-8')
if os.path.isdir(dirpath): if os.path.isdir(dirpath):
if ENFORCE_BASEPATH and not is_safe_path(BASEPATH.encode('utf-8'), dirpath):
raise OSError('Access denied.')
repo = None repo = None
activebranch = None activebranch = None
dirty = False dirty = False
@ -3536,13 +3555,14 @@ class RequestHandler(BaseHTTPRequestHandler):
'parent': os.path.dirname(os.path.abspath(dirpath)).decode('utf-8'), 'parent': os.path.dirname(os.path.abspath(dirpath)).decode('utf-8'),
'branches': branches, 'branches': branches,
'activebranch': activebranch, 'activebranch': activebranch,
'dirty': dirty 'dirty': dirty,
'error': None
} }
self.wfile.write(bytes(json.dumps(filedata), "utf8")) self.wfile.write(bytes(json.dumps(filedata), "utf8"))
except Exception as err: except Exception as err:
LOG.warning(err) LOG.warning(err)
content = str(err) content['error'] = str(err)
self.wfile.write(bytes(content, "utf8")) self.wfile.write(bytes(json.dumps(content), "utf8"))
return return
elif req.path.endswith('/api/abspath'): elif req.path.endswith('/api/abspath'):
content = "" content = ""

View file

@ -114,7 +114,7 @@
color: #616161 !important; color: #616161 !important;
font-weight: 400; font-weight: 400;
display: inline-block; display: inline-block;
width: 185px; width: 182px;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
cursor: pointer; cursor: pointer;
@ -2300,7 +2300,10 @@
function listdir(path) { function listdir(path) {
$.get(encodeURI("api/listdir?path=" + path), function(data) { $.get(encodeURI("api/listdir?path=" + path), function(data) {
renderpath(data); if (!data.error) {
renderpath(data);
}
console.log("Permission denied.");
}); });
document.getElementById("slide-out").scrollTop = 0; document.getElementById("slide-out").scrollTop = 0;
} }

View file

@ -2,6 +2,7 @@
"LISTENIP": "0.0.0.0", "LISTENIP": "0.0.0.0",
"LISTENPORT": 3218, "LISTENPORT": 3218,
"BASEPATH": null, "BASEPATH": null,
"ENFORCE_BASEPATH": false,
"SSL_CERTIFICATE": null, "SSL_CERTIFICATE": null,
"SSL_KEY": null, "SSL_KEY": null,
"HASS_API": "http://127.0.0.1:8123/api/", "HASS_API": "http://127.0.0.1:8123/api/",