Skip to content

Commit

Permalink
feat(security): add host verification to prevent hotlinking, see #123
Browse files Browse the repository at this point in the history
  • Loading branch information
a1ex4 committed Oct 21, 2024
1 parent 499678a commit 60daa5b
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 15 deletions.
22 changes: 20 additions & 2 deletions app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,25 @@ def index():

@tinfoil_access
def access_tinfoil_shop():
shop = gen_shop(db, app_settings)
shop = {}
# 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, remote access is disabled.")
return tinfoil_error(f"You are trying to access this shop with the `{request_host}` URL, but the shop URL is missing in Ownfoil configuration, remote access is disabled.\nPlease configure the shop URL to enable remote access and prevent someone else from stealing your shop.")

if 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}`.")

# enforce client side host verification
shop["referrer"] = f"https://{shop_host}"

shop.update(gen_shop(db, app_settings))
return jsonify(shop)

if all(header in request.headers for header in TINFOIL_HEADERS):
Expand All @@ -135,7 +153,7 @@ 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'])
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']))

@app.get('/api/settings')
@access_required('admin')
Expand Down
3 changes: 2 additions & 1 deletion app/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"public": False,
"encrypt": False,
"clientCertPub": "-----BEGIN PUBLIC KEY-----",
"clientCertKey": "-----BEGIN PRIVATE KEY-----"
"clientCertKey": "-----BEGIN PRIVATE KEY-----",
"url": "",
}
}

Expand Down
3 changes: 3 additions & 0 deletions app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +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
with open(CONFIG_FILE, 'w') as yaml_file:
yaml.dump(settings, yaml_file)
50 changes: 38 additions & 12 deletions app/templates/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ <h4 class="alert-heading">Missing admin account!</h4>
created, <strong>authentication is disabled, anyone can access and change the configuration of
your shop!</strong>
<br>
Add an admin account <a href="/settings#Authentication" class="alert-link">here under Authentication</a>.
Add an admin account <a href="/settings#Authentication" class="alert-link">here under
Authentication</a>.
</p>
</div>
{% endif %}
Expand All @@ -38,6 +39,16 @@ <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>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}

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

<table class="table table-hover caption-top" id="userTable">
Expand Down Expand Up @@ -145,8 +156,8 @@ <h2 class="pb-3">Library</h2>
</div>

<div class="col-auto">
<button type="button" class="btn btn-primary mb-3 scanBtn"
onClick='scanLibrary()'>Scan library</button>
<button type="button" class="btn btn-primary mb-3 scanBtn" onClick='scanLibrary()'>Scan
library</button>
</div>
</div>

Expand All @@ -166,7 +177,8 @@ <h2 class="pb-3">Library</h2>
</div>

<!-- Delete User Modal -->
<div class="modal fade" id="deletePathModal" tabindex="-1" aria-labelledby="deletePathModalLabel" aria-hidden="true">
<div class="modal fade" id="deletePathModal" tabindex="-1" aria-labelledby="deletePathModalLabel"
aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
Expand Down Expand Up @@ -227,7 +239,18 @@ <h2 class="pb-3">Titles</h2>
<hr>

<h2 class="pb-3">Shop</h2>
<p>Customize your shop:</p>
<!-- 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
<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.
</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="publicShopCheck"
aria-describedby="publicShopCheckHelp">
Expand All @@ -240,7 +263,8 @@ <h2 class="pb-3">Shop</h2>
<input type="checkbox" class="form-check-input" id="encryptShopCheck"
aria-describedby="encryptShopCheckHelp" disabled="disabled">
<label class="form-check-label" for="encryptShopCheck">Encrypt shop</label>
<div id="encryptShopCheckHelp" class="form-text">Serve encrypted shop, so that only Tinfoil clients can access the content (coming soon).</div>
<div id="encryptShopCheckHelp" class="form-text">Serve encrypted shop, so that only Tinfoil clients
can access the content (coming soon).</div>
</div>

<div class="mb-3">
Expand Down Expand Up @@ -281,7 +305,7 @@ <h2 class="pb-3">Shop</h2>
return $('#' + checkboxId).is(":checked")
}

function scanLibrary(path=null) {
function scanLibrary(path = null) {
$('.scanBtn').prop('disabled', true);
data = {
path: path
Expand Down Expand Up @@ -318,7 +342,7 @@ <h2 class="pb-3">Shop</h2>
}
}
paths = result.paths;
if ( paths === null || !paths.length) {
if (paths === null || !paths.length) {
$('#pathsTable tbody').append(
'<tr><td>-</td><td>-</td><td>');
} else {
Expand Down Expand Up @@ -474,11 +498,11 @@ <h2 class="pb-3">Shop</h2>
$('#checkboxNewUserAdminAccess').change(function () {
if (this.checked != false) {
$('#checkboxNewUserShopAccess').prop('checked', true).attr("disabled", true);
$('#checkboxNewUserBackupAccess').prop('checked', true).attr("disabled", true) ;
$('#checkboxNewUserBackupAccess').prop('checked', true).attr("disabled", true);

} else {
$('#checkboxNewUserShopAccess').attr("disabled", false);
$('#checkboxNewUserBackupAccess').attr("disabled", false) ;
$('#checkboxNewUserBackupAccess').attr("disabled", false);

}
});
Expand Down Expand Up @@ -564,7 +588,7 @@ <h2 class="pb-3">Shop</h2>
} else {
console.log('Error adding path ' + path);
}
$('#submitNewLibraryPathBtn').prop('disabled', false);
$('#submitNewLibraryPathBtn').prop('disabled', false);
}
});
}
Expand Down Expand Up @@ -627,6 +651,7 @@ <h2 class="pb-3">Shop</h2>
function submitShopSettings() {
$('#submitShopSettingsBtn').prop('disabled', true);
data = {
url: $('#shopUrlInput').val(),
public: getCheckboxStatus("publicShopCheck"),
encrypt: getCheckboxStatus("encryptShopCheck"),
motd: $('#motdTextArea').val(),
Expand All @@ -650,7 +675,7 @@ <h2 class="pb-3">Shop</h2>
$("#" + formTextElement).text(error['error']);
});
}
$('#submitShopSettingsBtn').prop('disabled', false);
$('#submitShopSettingsBtn').prop('disabled', false);
}
});
}
Expand Down Expand Up @@ -686,6 +711,7 @@ <h2 class="pb-3">Shop</h2>

// Shop settings
shopSettings = result['shop'];
setInputVal("shopUrlInput", shopSettings['url']);
setInputVal("motdTextArea", shopSettings['motd']);
$("#publicShopCheck").prop("checked", shopSettings['public']);
$("#encryptShopCheck").prop("checked", shopSettings['encrypt']);
Expand Down

0 comments on commit 60daa5b

Please sign in to comment.