Skip to content
Snippets Groups Projects
Commit 3810b0db authored by Markus Willman's avatar Markus Willman
Browse files

fix issues from ICT showroom

- use a strict CSP (that actually works), blocks youtube media etc.
- clock updates less frequently save on CPU cycles
- fix frame cover height in kiosk mode
parent 1f7c56ee
No related branches found
No related tags found
No related merge requests found
......@@ -9,7 +9,7 @@ from datetime import timedelta
from dateutil.tz import tzlocal
from .staticVariables import TEMPLATE_DIR, STATIC_DIR, DATA_DIR, DEFAULT_STOP, DEFAULT_HOST, DEFAULT_PORT, APP_VERSION, AVAILABLE_LANGUAGES, TRANSLATION_DIR, GOOGLE_MAPS_KEY, GOOGLE_MAPS_TEST_KEY, GOOGLE_MAPS_REGION
from .dataHandler import loadJson, hasTemplate, hasData, isDirectory, CustomJsonEncoder
from .dataHandler import loadJson, hasTemplate, hasData, isDirectory, CustomJsonEncoder, generateToken
from .gtfs import GtfsHandler, GtfsConstants
from .locator import FoliLocator
......@@ -91,11 +91,25 @@ def afterRequestHandler(response):
response.headers['Server'] = APP_VERSION
response.headers['X-Frame-Options'] = 'DENY'
response.headers['X-XSS-Protection'] = '1; mode=block'
response.headers['Content-Security-Policy'] = "default-src='self'"
# include a strict'ish list of dependencies, deliberately don't allow media sources such as youtube, this allows safer use of the twitter widget (assumes modern browser)
cspSrcWhitelist = "*.twitter.com *.twimg.com *.googleapis.com script.google.com script.googleusercontent.com fonts.gstatic.com maxcdn.bootstrapcdn.com cdnjs.cloudflare.com code.jquery.com www.feedrapp.info weatherwidget.io forecast7.com"
if not getattr(g, 'is_kiosk', False):
cspSrcWhitelist += " *.youtube.com"
cspPolicy = "default-src 'self' 'unsafe-inline' 'unsafe-eval' {whitelist}; img-src http: https: data:; base-uri 'none'; object-src 'none'; script-src 'self' 'nonce-{nonce}' 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' {whitelist}".format(nonce = g.csp_nonce, whitelist = cspSrcWhitelist)
if getattr(g, 'is_kiosk', False):
cspPolicy += "; media-src 'none'"
else:
cspPolicy += "; media-src *"
response.headers['Content-Security-Policy'] = cspPolicy
return response
@app.url_value_preprocessor
def pullUrlGlobals(endpoint, values):
g.csp_nonce = generateToken(16)
if endpoint == None or values == None or endpoint == 'static' or endpoint.startswith('root/'):
return
......
# coding=utf-8
import os, json, sys
import os, json, sys, base64
from flask.json import JSONEncoder
from datetime import date
......@@ -61,3 +61,7 @@ def hasTemplate(filename):
def hasData(filename):
path = os.path.join(DATA_DIR, filename)
return not os.path.islink(path) and os.path.isfile(os.path.normpath(path))
# python token_urlsafe backported
def generateToken(bytes):
return base64.urlsafe_b64encode(os.urandom(bytes)).rstrip(b'=').decode('ascii')
......@@ -204,10 +204,10 @@
{% block scripts %}
{{- super() }}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-serialize-object/2.5.0/jquery.serialize-object.min.js"
integrity="sha256-E8KRdFk/LTaaCBoQIV/rFNc0s3ICQQiOHFT4Cioifa8=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-serialize-object/2.5.0/jquery.serialize-object.min.js" integrity="sha256-E8KRdFk/LTaaCBoQIV/rFNc0s3ICQQiOHFT4Cioifa8="
crossorigin="anonymous" nonce="{{ g.csp_nonce }}"></script>
<script>
<script nonce="{{ g.csp_nonce }}">
$(document).ready(function () {
// this may require a GET request, because of browser security rules...
// POST would be ideal, however, since we are only sending numeric values it doesn't really matter
......
......@@ -15,7 +15,7 @@
<div class="card-header">
<h3 class="card-title">Bussilippu kännykällä</h3>
</div>
<div class="row" style="margin-top: 25px;">
<div class="row" class="mt-3">
<div class="col-sm-5">
<img class="app-image" src="{{ util.static_url('img/mobiililippu_1.png') }}">
</div>
......
......@@ -13,7 +13,7 @@
<div class="card foli-card box-shadow content-spaced">
<div class="card-body">
<p style="margin: 0;">
<p class="m-0">
Bussissa voit ostaa kertalipun tai ladata matkakortin käteisellä. Samalla lipulla tai matkakortilla
on vaihtoaikaa 2 tuntia.
</p>
......
......@@ -8,7 +8,7 @@
{% block scripts %}
{{- super() }}
<script>
<script nonce="{{ g.csp_nonce }}">
$(function () {
$('.menu-card').clickableBox();
});
......
......@@ -6,7 +6,11 @@
{% set content_lang = 'fi' %}
{% block head %}
<meta name="twitter:widgets:csp" content="on">
<meta name="twitter:widgets:autoload" content="off">
{{- super() }}
<style>
.h-auto { height: auto; }
.info-card > #visitTurku { overflow-y: scroll; }
......@@ -17,7 +21,7 @@
but kiosk mode need not be responsive, since the screen resolution is fixed */
.iframe-container {
width: 100%;
height: 100px;
height: 120px;
position: relative;
}
.iframe-area, .iframe-cover {
......@@ -55,7 +59,7 @@
<div class="iframe-area">
<div id="forecast7">
<a class="weatherwidget-io" href="https://forecast7.com/en/60d4522d27/turku/" data-label_1="TURKU" data-label_2="Sää" data-theme="original">TURKU Sää</a>
<script>
<script nonce="{{ g.csp_nonce }}">
!function (d, s, id) { var js, fjs = d.getElementsByTagName(s)[0]; if (!d.getElementById(id)) { js = d.createElement(s); js.id = id; js.src = 'https://weatherwidget.io/js/widget.min.js'; fjs.parentNode.insertBefore(js, fjs); } }(document, 'script', 'weatherwidget-io-js');
</script>
</div>
......@@ -96,9 +100,9 @@
{{- super() }}
<!-- TODO: once the cdnjs project hosts this, migrate to that -->
<script src="{{ util.static_url('js/jquery-rss/3.3.0/jquery.rss.min.js') }}"></script>
<script src="{{ util.static_url('js/jquery-rss/3.3.0/jquery.rss.min.js') }}" nonce="{{ g.csp_nonce }}"></script>
<script>
<script nonce="{{ g.csp_nonce }}">
$(document).ready(function () {
// Twitter initialization code
window.twttr = (function (d, s, id) {
......@@ -139,7 +143,11 @@
$('#twitter').disableExternalLinks(); // in case the widget fails to load disable the link in the container as a placeholder
$('small.source-link').disableExternalLinks();
// Note: disableExternalLinks() is not actually enough here, youtube videos and other embedded media are beyond its reach
// HTML5 sandboxing on the iframe may help, but it is more likely just to break the widget...
twttr.ready(function (twttr) {
twttr.widgets.load(document.getElementById("twitter"));
twttr.events.bind('loaded', function (e) {
e.widgets.forEach(function (w) {
// the twitter widget is loaded in an iframe, contents() is needed because of that
......
......@@ -139,7 +139,7 @@
</div>
</div>
<small class="text-muted">{{ _("This application aggregates information from several sources, using open data or third party services, we take no responsibility on the accuracy of any such information authored by those third parties.") }}<small>
<small class="text-muted">{{ _("This application aggregates information from several sources, using open data or third party services, we take no responsibility on the accuracy of any such information authored by those third parties.") }}</small>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">{{ _("Close") }}</button>
......@@ -179,21 +179,21 @@
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
crossorigin="anonymous"></script>
crossorigin="anonymous" nonce="{{ g.csp_nonce }}"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
crossorigin="anonymous"></script>
crossorigin="anonymous" nonce="{{ g.csp_nonce }}"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
crossorigin="anonymous"></script>
crossorigin="anonymous" nonce="{{ g.csp_nonce }}"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.qrcode/1.0/jquery.qrcode.min.js" integrity="sha256-9MzwK2kJKBmsJFdccXoIDDtsbWFh8bjYK/C7UjB1Ay0="
crossorigin="anonymous"></script>
crossorigin="anonymous" nonce="{{ g.csp_nonce }}"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.20.1/moment-with-locales.min.js" integrity="sha256-XWrGUqSiENmD8bL+BVeLl7iCfhs+pkPyIqrZQcS2Te8="
crossorigin="anonymous"></script>
crossorigin="anonymous" nonce="{{ g.csp_nonce }}"></script>
{% if stop_info.stop_timezone %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.14/moment-timezone-with-data.min.js" integrity="sha256-FJZOELgwnfQRdG8KZUSWCYgucECDf4w5kfQdQSGbVpI="
crossorigin="anonymous"></script>
crossorigin="anonymous" nonce="{{ g.csp_nonce }}"></script>
{% endif %}
<script>
<script nonce="{{ g.csp_nonce }}">
// quick and dirty jquery "plugin" for making block level elements clickable by using a link they contain
// TODO: this needs some serious refinement once we know how we implement changing of the views so that the whole page doesn't need to reload
// FIXME: does not chain to previous click handler if it is already set!
......@@ -214,6 +214,8 @@
var handler = function (e) {
e.preventDefault();
e.stopPropagation();
$link = $(this);
$modal = $('#externalLinkModal');
......@@ -227,10 +229,10 @@
};
// the two slashes are for the benefit of urls that contain refering URI in the query string
this.on('click', 'a[href^="http"]:not([href*="//{{ request.host }}"])', handler);
this.on('click', 'a[href^="http"]:not([href*="https://{{ request.host }}"])', handler);
// disable links for foreign protocols, just in case
this.on('click', 'a[href^="mailto:"]', function(e) { e.preventDefault(); });
this.on('click', 'a[href*="://"]:not([href^="http"])', function(e) { e.preventDefault(); });
this.on('click', 'a[href^="mailto:"]', function(e) { e.preventDefault(); e.stopPropagation(); });
this.on('click', 'a[href*="://"]:not([href^="http"])', function(e) { e.preventDefault(); e.stopPropagation(); });
// avoid duplicate calls (see info page, twitter widget)
this.data('links-disabled', true);
......@@ -258,16 +260,18 @@
// clock implementation follows
$clock = $('#headerClock');
var clockFormat = 'H:mm';
var timeOffset = moment(new Date($clock.data('server-time'))).diff(moment());
var currentTime;
var timerHandler = function() {
currentTime = moment().add(timeOffset);
$clock.html(currentTime{% if stop_info.stop_timezone %}.tz('{{ stop_info.stop_timezone }}'){% endif %}.format(clockFormat));
newTime = currentTime{% if stop_info.stop_timezone %}.tz('{{ stop_info.stop_timezone }}'){% endif %}.format('H:mm');
if (newTime !== $clock.html())
$clock.html(newTime);
// run on next second wrap
setTimeout(timerHandler, (1000 - (new Date().getTime() % 1000)));
// run on next 5 second wrap to reduce unnecessary cycles
setTimeout(timerHandler, (5000 - (new Date().getTime() % 5000)));
};
// start the clock
......
......@@ -6,6 +6,7 @@
{% block head %}
{{- super() }}
<style>
#map-canvas, #map-canvas > div { border-radius: 0.25rem; }
</style>
......@@ -68,13 +69,14 @@
{% block scripts %}
{{- super() }}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-easing/1.4.1/jquery.easing.min.js"></script>
<script src="https://maps.googleapis.com/maps/api/js?key={{ gmaps_key }}&amp;region={{ gmaps_region }}&amp;language={{ g.lang_code }}"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-easing/1.4.1/jquery.easing.min.js" nonce="{{ g.csp_nonce }}"></script>
<script src="https://maps.googleapis.com/maps/api/js?key={{ gmaps_key }}&amp;region={{ gmaps_region }}&amp;language={{ g.lang_code }}" nonce="{{ g.csp_nonce }}"></script>
<!-- this is used to animate the movement of the buses on the map -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/marker-animate-unobtrusive/0.2.8/vendor/markerAnimate.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marker-animate-unobtrusive/0.2.8/SlidingMarker.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marker-animate-unobtrusive/0.2.8/vendor/markerAnimate.js" nonce="{{ g.csp_nonce }}"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marker-animate-unobtrusive/0.2.8/SlidingMarker.min.js" nonce="{{ g.csp_nonce }}"></script>
<script>
<script nonce="{{ g.csp_nonce }}">
var gmarkers, gmap = false;
// focusedMarker is the title of the focused marker
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment