Skip to content

Commit

Permalink
feat(security): implement host signature verification for secure acce…
Browse files Browse the repository at this point in the history
…ss from Tinfoil
  • Loading branch information
a1ex4 committed Dec 9, 2024
1 parent 2fd65de commit 4d96d75
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 70 deletions.
111 changes: 65 additions & 46 deletions app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,57 @@ def tinfoil_error(error):
def tinfoil_access(f):
@wraps(f)
def _tinfoil_access(*args, **kwargs):
reload_conf()
hauth_success = None
auth_success = None
request.verified_host = None
# Host verification to prevent hotlinking
host_verification = request.is_secure or request.headers.get("X-Forwarded-Proto") == "https"
if host_verification:
request_host = request.host
request_hauth = request.headers.get('Hauth')
logger.info(f"Secure Tinfoil request from remote host {request_host}, proceeding with host verification.")
shop_host = app_settings["shop"].get("host")
shop_hauth = app_settings["shop"].get("hauth")
if not shop_host:
logger.error("Missing shop host configuration, Host verification is disabled.")

elif request_host != shop_host:
logger.warning(f"Incorrect URL referrer detected: {request_host}.")
error = f"Incorrect URL `{request_host}`."
hauth_success = False

elif not shop_hauth:
# Try authentication, if an admin user is logging in then set the hauth
auth_success, auth_error, auth_is_admin = basic_auth(request)
if auth_success and auth_is_admin:
shop_settings = app_settings['shop']
shop_settings['hauth'] = request_hauth
set_shop_settings(shop_settings)
logger.info(f"Successfully set Hauth value for host {request_host}.")
hauth_success = True
else:
logger.warning(f"Hauth value not set for host {request_host}, Host verification is disabled. Connect to the shop from Tinfoil with an admin account to set it.")

elif request_hauth != shop_hauth:
logger.warning(f"Incorrect Hauth detected for host: {request_host}.")
error = f"Incorrect Hauth for URL `{request_host}`."
hauth_success = False

else:
hauth_success = True
request.verified_host = shop_host

if hauth_success is False:
return tinfoil_error(error)

# Now checking auth if shop is private
if not app_settings['shop']['public']:
# Shop is private
success, error = basic_auth(request)
if not success:
return tinfoil_error(error)
if auth_success is None:
auth_success, auth_error, _ = basic_auth(request)
if not auth_success:
return tinfoil_error(auth_error)
# Auth success
return f(*args, **kwargs)
return _tinfoil_access
Expand All @@ -120,22 +166,10 @@ def access_tinfoil_shop():
shop = {
"success": app_settings['shop']['motd']
}
# Host verification to prevent hotlinking
request_host = request.host
host_verification = request.is_secure or request.headers.get("X-Forwarded-Proto") == "https"
if host_verification:
logger.info(f"Secure access with remote host {request_host}, proceeding with host verification")
shop_host = app_settings["shop"].get("url")
if not shop_host:
logger.error("Missing shop URL configuration, Host verification is disabled.")
shop["error"] = f"You are trying to access this shop with the `{request_host}` URL, but the shop URL is missing in Ownfoil configuration.\nPlease configure the shop URL to secure remote access and prevent someone else from stealing your shop."

elif request_host != shop_host:
logger.warning(f"Incorrect URL referrer detected: {request_host}.")
return tinfoil_error(f"Incorrect URL `{request_host}`.\nSomeone is trying to steal from the shop with original URL `{shop_host}`.")
else:
# enforce client side host verification
shop["referrer"] = f"https://{shop_host}"

if request.verified_host is not None:
# enforce client side host verification
shop["referrer"] = f"https://{request.verified_host}"

shop["files"] = gen_shop_files(db)
return jsonify(shop)
Expand All @@ -155,13 +189,23 @@ def settings_page():
with open(os.path.join(TITLEDB_DIR, 'languages.json')) as f:
languages = json.load(f)
languages = dict(sorted(languages.items()))
return render_template('settings.html', title='Settings', languages_from_titledb=languages, admin_account_created=admin_account_created(), valid_keys=app_settings['titles']['valid_keys'], url_set=bool(app_settings['shop']['url']))
return render_template(
'settings.html',
title='Settings',
languages_from_titledb=languages,
admin_account_created=admin_account_created(),
valid_keys=app_settings['titles']['valid_keys'])

@app.get('/api/settings')
@access_required('admin')
def get_settings_api():
reload_conf()
return jsonify(app_settings)
settings = app_settings
if settings['shop'].get('hauth'):
settings['shop']['hauth'] = True
else:
settings['shop']['hauth'] = False
return jsonify(settings)

@app.post('/api/settings/titles')
@access_required('admin')
Expand Down Expand Up @@ -401,31 +445,6 @@ def on_library_change(events):
remove_missing_files()
titles_library = generate_library()

@app.before_request
def before_request():
# Code to run before each request
print("Before request")
# print(f"access_route: {request.access_route}")
# print(f"args: {request.args}")
# print(f"base_url: {request.base_url}")
# print(f"data: {request.data}")
# print(f"full_path: {request.full_path}")
# print(f"host: {request.host}")
# print(f"host_url: {request.host_url}")
# print(f"is_json: {request.is_json}")
# print(f"is_secure: {request.is_secure}")
# # print(f"json: {request.json}")
# print(f"method: {request.method}")
# print(f"path: {request.path}")
# print(f"query_string: {request.query_string}")
# print(f"referrer: {request.referrer}")
# print(f"remote_addr: {request.remote_addr}")
# print(f"scheme: {request.scheme}")
# print(f"url_root: {request.url_root}")
# print(f"url: {request.url}")
# print(f"user_agent: {request.user_agent}")
# print(f"trusted_hosts: {request.trusted_hosts}")
# print(request.headers)

if __name__ == '__main__':
logger.info('Starting initialization of Ownfoil...')
Expand Down
5 changes: 4 additions & 1 deletion app/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def decorated_view(*args, **kwargs):
def basic_auth(request):
success = True
error = ''
is_admin = False

auth = request.authorization
if auth is None:
Expand All @@ -84,7 +85,9 @@ def basic_auth(request):
success = False
error = f'User "{username}" does not have access to the shop.'

return success, error
else:
is_admin = user.has_admin_access()
return success, error, is_admin

auth_blueprint = Blueprint('auth', __name__)

Expand Down
3 changes: 2 additions & 1 deletion app/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
"encrypt": False,
"clientCertPub": "-----BEGIN PUBLIC KEY-----",
"clientCertKey": "-----BEGIN PRIVATE KEY-----",
"url": "",
"host": "",
"hauth": "",
}
}

Expand Down
8 changes: 4 additions & 4 deletions app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,9 @@ def set_titles_settings(region, language):

def set_shop_settings(data):
settings = load_settings()
shop_url = data['url']
if '://' in shop_url:
data['url'] = shop_url.split('://')[-1]
settings['shop'] = data
shop_host = data['host']
if '://' in shop_host:
data['host'] = shop_host.split('://')[-1]
settings['shop'].update(data)
with open(CONFIG_FILE, 'w') as yaml_file:
yaml.dump(settings, yaml_file)
44 changes: 26 additions & 18 deletions app/templates/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,22 @@ <h4 class="alert-heading">Missing console keys!</h4>
</div>
{% endif %}

{% if url_set == false %}
<div id="missingShopUrlAlert" class="alert alert-warning alert-dismissible fade show" role="alert">
<h4 class="alert-heading">Missing shop URL!</h4>
Remote access from outside the local network is disabled if the shop URL is missing, configure it <a
href="/settings#shopUrlInput" class="alert-link">here
in Ownfoil.</a>
<div id="missingShopHostAlert" class="alert alert-warning alert-dismissible fade show" role="alert" style="display: none;">
<h4 class="alert-heading">Missing shop Host!</h4>
Host verification from outside the local network is disabled if the shop <code>Host</code> is
missing, configure it <a href="/settings#shopHostInput" class="alert-link">here
in Ownfoil</a> to prevent someone else stealing from your shop.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>

<div id="missingShopHauthAlert" class="alert alert-warning alert-dismissible fade show" role="alert" style="display: none;">
<h4 class="alert-heading">Missing shop Hauth!</h4>
<a href="https://blawar.github.io/tinfoil/network/#host-signature" class="alert-link"
target="_blank">Host signature</a> verification from outside the local network is disabled if
the shop <code>Hauth</code> is missing. Connect to the shop from Tinfoil with an admin account to
set it.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}

<h2 id="Authentication" class="pb-3">Authentication</h2>

Expand Down Expand Up @@ -241,11 +248,11 @@ <h2 class="pb-3">Titles</h2>
<h2 class="pb-3">Shop</h2>
<!-- Shop URL -->
<div class="mb-3">
<label for="shopUrlInput" class="form-label">Shop URL:</label>
<input class="form-control" id="shopUrlInput" aria-describedby="shopUrlHelp" placeholder="shop.domain.tld">
<div id="shopUrlHelp" class="form-text">Configure your shop URL to enable remote access (from
outside of the local network).<br>
URL verification from Tinfoil requests will be enforced if the shop is accessed securely (with
<label for="shopHostInput" class="form-label">Shop URL:</label>
<input class="form-control" id="shopHostInput" aria-describedby="shopHostHelp"
placeholder="shop.domain.tld">
<div id="shopHostHelp" class="form-text">Configure your shop URL to enable host verification.<br>
Host verification from Tinfoil requests will be enforced if the shop is accessed securely (with
<code>https</code>), make sure your reverse proxy <strong>does NOT</strong> allow insecure
connections (or upgrades them to use SSL) and correctly sets the <code>X-Forwarded-Proto</code>
header.
Expand Down Expand Up @@ -334,7 +341,6 @@ <h2 class="pb-3">Shop</h2>

$.getJSON("/api/settings/library/paths", function (result) {

console.log(result)
if (!result['success']) {
if (result['status_code'] == '302') {
window.location.href = result['location']
Expand Down Expand Up @@ -375,7 +381,6 @@ <h2 class="pb-3">Shop</h2>
'<tr><td>-</td><td>-</td><td>-</td></tr>');
}
result.forEach(user => {
console.log(user)
allUsernames.push(user['user']);
base_input = '<input class="form-check-input" type="checkbox" onclick="return false" checked>';
shop_input = base_input;
Expand Down Expand Up @@ -569,7 +574,6 @@ <h2 class="pb-3">Shop</h2>
data = {
path: path
}
console.log(data)

$.ajax({
url: "/api/settings/library/paths",
Expand Down Expand Up @@ -613,7 +617,6 @@ <h2 class="pb-3">Shop</h2>
processData: false,
success: function (result) {
if (result['success']) {
console.log(result)
$('#consoleKeysInput').removeClass('is-invalid');
$('#consoleKeysInput').addClass('is-valid');
$('#missingKeysAlert').addClass('d-none');
Expand Down Expand Up @@ -651,7 +654,7 @@ <h2 class="pb-3">Shop</h2>
function submitShopSettings() {
$('#submitShopSettingsBtn').prop('disabled', true);
data = {
url: $('#shopUrlInput').val(),
host: $('#shopHostInput').val(),
public: getCheckboxStatus("publicShopCheck"),
encrypt: getCheckboxStatus("encryptShopCheck"),
motd: $('#motdTextArea').val(),
Expand Down Expand Up @@ -711,10 +714,15 @@ <h2 class="pb-3">Shop</h2>

// Shop settings
shopSettings = result['shop'];
setInputVal("shopUrlInput", shopSettings['url']);
setInputVal("shopHostInput", shopSettings['host']);
setInputVal("motdTextArea", shopSettings['motd']);
$("#publicShopCheck").prop("checked", shopSettings['public']);
$("#encryptShopCheck").prop("checked", shopSettings['encrypt']);
if (!shopSettings['host']) {
$("#missingShopHostAlert").css('display', '');
} else if (!shopSettings['hauth']) {
$("#missingShopHauthAlert").css('display', '');
}
});

});
Expand Down

0 comments on commit 4d96d75

Please sign in to comment.