Add Content Security Policy meta tags

このコミットが含まれているのは:
n9k 2022-03-07 07:11:49 +00:00
コミット 4bab173237
12個のファイルの変更89行の追加26行の削除

ファイルの表示

@ -5,11 +5,16 @@ from anonstream.segments import segments
from anonstream.stream import is_online, get_stream_uptime from anonstream.stream import is_online, get_stream_uptime
from anonstream.user import watched from anonstream.user import watched
from anonstream.routes.wrappers import with_user_from, auth_required from anonstream.routes.wrappers import with_user_from, auth_required
from anonstream.utils.security import generate_csp
@current_app.route('/') @current_app.route('/')
@with_user_from(request) @with_user_from(request)
async def home(user): async def home(user):
return await render_template('home.html', user=user) return await render_template(
'home.html',
csp=generate_csp(),
user=user,
)
@current_app.route('/stream.mp4') @current_app.route('/stream.mp4')
@with_user_from(request) @with_user_from(request)

ファイルの表示

@ -8,6 +8,7 @@ from anonstream.routes.wrappers import with_user_from, render_template_with_etag
from anonstream.helpers.chat import get_scrollback from anonstream.helpers.chat import get_scrollback
from anonstream.helpers.user import get_default_name from anonstream.helpers.user import get_default_name
from anonstream.utils.chat import generate_nonce from anonstream.utils.chat import generate_nonce
from anonstream.utils.security import generate_csp
from anonstream.utils.user import concatenate_for_notice from anonstream.utils.user import concatenate_for_notice
CONFIG = current_app.config CONFIG = current_app.config
@ -18,6 +19,7 @@ USERS_BY_TOKEN = current_app.users_by_token
async def nojs_stream(user): async def nojs_stream(user):
return await render_template( return await render_template(
'nojs_stream.html', 'nojs_stream.html',
csp=generate_csp(),
user=user, user=user,
) )
@ -28,6 +30,7 @@ async def nojs_info(user):
uptime, viewership = get_stream_uptime_and_viewership() uptime, viewership = get_stream_uptime_and_viewership()
return await render_template( return await render_template(
'nojs_info.html', 'nojs_info.html',
csp=generate_csp(),
user=user, user=user,
viewership=viewership, viewership=viewership,
uptime=uptime, uptime=uptime,
@ -40,6 +43,7 @@ async def nojs_info(user):
async def nojs_chat_messages(user): async def nojs_chat_messages(user):
return await render_template_with_etag( return await render_template_with_etag(
'nojs_chat_messages.html', 'nojs_chat_messages.html',
{'csp': generate_csp()},
user=user, user=user,
users_by_token=USERS_BY_TOKEN, users_by_token=USERS_BY_TOKEN,
messages=get_scrollback(current_app.messages), messages=get_scrollback(current_app.messages),
@ -58,6 +62,7 @@ async def nojs_chat_users(user):
users_by_presence = get_users_by_presence() users_by_presence = get_users_by_presence()
return await render_template_with_etag( return await render_template_with_etag(
'nojs_chat_users.html', 'nojs_chat_users.html',
{'csp': generate_csp()},
user=user, user=user,
get_default_name=get_default_name, get_default_name=get_default_name,
users_watching=users_by_presence[Presence.WATCHING], users_watching=users_by_presence[Presence.WATCHING],
@ -73,6 +78,7 @@ async def nojs_chat_form(user):
prefer_chat_form = request.args.get('landing') != 'appearance' prefer_chat_form = request.args.get('landing') != 'appearance'
return await render_template( return await render_template(
'nojs_chat_form.html', 'nojs_chat_form.html',
csp=generate_csp(),
user=user, user=user,
state=state, state=state,
prefer_chat_form=prefer_chat_form, prefer_chat_form=prefer_chat_form,

ファイルの表示

@ -86,11 +86,16 @@ def with_user_from(context):
return with_user_from_context return with_user_from_context
async def render_template_with_etag(*args, **kwargs): async def render_template_with_etag(template, deferred_kwargs, **kwargs):
rendered_template = await render_template(*args, **kwargs) render = await render_template(template, **kwargs)
tag = hashlib.sha256(rendered_template.encode()).hexdigest() tag = hashlib.sha256(render.encode()).hexdigest()
etag = f'W/"{tag}"' etag = f'W/"{tag}"'
if request.if_none_match.contains_weak(tag): if request.if_none_match.contains_weak(tag):
return '', 304, {'ETag': etag} return '', 304, {'ETag': etag}
else: else:
rendered_template = await render_template(
template,
**deferred_kwargs,
**kwargs,
)
return rendered_template, {'ETag': etag} return rendered_template, {'ETag': etag}

ファイルの表示

@ -2,10 +2,10 @@
const TOKEN = document.body.dataset.token; const TOKEN = document.body.dataset.token;
const TOKEN_HASH = document.body.dataset.tokenHash; const TOKEN_HASH = document.body.dataset.tokenHash;
/* Content Security Policy nonce */
const CSP = document.body.dataset.csp;
/* insert js-only markup */ /* insert js-only markup */
const jsmarkup_style_color = '<style id="style-color"></style>'
const jsmarkup_style_tripcode_display = '<style id="style-tripcode-display"></style>'
const jsmarkup_style_tripcode_colors = '<style id="style-tripcode-colors"></style>'
const jsmarkup_stream = `<video id="stream_js" src="/stream.mp4?token=${encodeURIComponent(TOKEN)}" autoplay controls></video>` const jsmarkup_stream = `<video id="stream_js" src="/stream.mp4?token=${encodeURIComponent(TOKEN)}" autoplay controls></video>`
const jsmarkup_info = '<div id="info_js" data-js="true"></div>'; const jsmarkup_info = '<div id="info_js" data-js="true"></div>';
const jsmarkup_info_float = '<aside id="info_js__float"></aside>'; const jsmarkup_info_float = '<aside id="info_js__float"></aside>';
@ -57,18 +57,24 @@ const jsmarkup_chat_form = `\
</div> </div>
</form>`; </form>`;
const insert_jsmarkup = () => {jsmarkup_info_float_viewership const insert_jsmarkup = () => {
if (document.getElementById("style-color") === null) { if (document.getElementById("style-color") === null) {
const parent = document.head; const style_color = document.createElement("style");
parent.insertAdjacentHTML("beforeend", jsmarkup_style_color); style_color.id = "style-color";
style_color.nonce = CSP;
document.head.insertAdjacentElement("beforeend", style_color);
} }
if (document.getElementById("style-tripcode-display") === null) { if (document.getElementById("style-tripcode-display") === null) {
const parent = document.head; const style_tripcode_display = document.createElement("style");
parent.insertAdjacentHTML("beforeend", jsmarkup_style_tripcode_display); style_tripcode_display.id = "style-tripcode-display";
style_tripcode_display.nonce = CSP;
document.head.insertAdjacentElement("beforeend", style_tripcode_display);
} }
if (document.getElementById("style-tripcode-colors") === null) { if (document.getElementById("style-tripcode-colors") === null) {
const parent = document.head; const style_tripcode_colors = document.createElement("style");
parent.insertAdjacentHTML("beforeend", jsmarkup_style_tripcode_colors); style_tripcode_colors.id = "style-tripcode-colors";
style_tripcode_colors.nonce = CSP;
document.head.insertAdjacentElement("beforeend", style_tripcode_colors);
} }
if (document.getElementById("stream_js") === null) { if (document.getElementById("stream_js") === null) {
const parent = document.getElementById("stream"); const parent = document.getElementById("stream");

ファイルの表示

@ -3,9 +3,10 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="content-security-policy" content="default-src 'none'; connect-src 'self'; img-src 'self'; frame-src 'self'; media-src 'self'; script-src 'self'; style-src 'self' 'nonce-{{ csp }}';">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}" type="text/css"> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}" type="text/css">
</head> </head>
<body id="both" data-token="{{ user.token }}" data-token-hash="{{ user.token_hash }}"> <body id="both" data-token="{{ user.token }}" data-token-hash="{{ user.token_hash }}" data-csp="{{ csp }}">
<article id="stream"> <article id="stream">
<noscript><iframe id="stream_nojs" name="stream_nojs" src="{{ url_for('nojs_stream', token=user.token) }}"></iframe></noscript> <noscript><iframe id="stream_nojs" name="stream_nojs" src="{{ url_for('nojs_stream', token=user.token) }}"></iframe></noscript>
</article> </article>

ファイルの表示

@ -13,7 +13,7 @@
<b class="{{ insignia_class }}" title="Broadcaster">##</b> <b class="{{ insignia_class }}" title="Broadcaster">##</b>
{{- '&nbsp;' | safe -}} {{- '&nbsp;' | safe -}}
{%- endif -%} {%- endif -%}
<span class="{{ name_class }}" style="color:{{ user.color }};"> <span class="{{ name_class }}">
{{- user.name or get_default_name(user) -}} {{- user.name or get_default_name(user) -}}
{%- if not user.broadcaster and user.name is none -%} {%- if not user.broadcaster and user.name is none -%}
<sup class="{{ tag_class }}"><b>{{ user.tag }}</b></sup> <sup class="{{ tag_class }}"><b>{{ user.tag }}</b></sup>
@ -22,6 +22,6 @@
{%- if user.tripcode -%} {%- if user.tripcode -%}
<span class="{{ tripcode_nbsp_class }}">&nbsp;</span> <span class="{{ tripcode_nbsp_class }}">&nbsp;</span>
{{- '' -}} {{- '' -}}
<span class="{{ tripcode_class }}" style="background-color:{{ user.tripcode.background_color }};color:{{ user.tripcode.foreground_color }};">{{ user.tripcode.digest }}</span> <span class="{{ tripcode_class }}">{{ user.tripcode.digest }}</span>
{%- endif -%} {%- endif -%}
{% endmacro %} {% endmacro %}

ファイルの表示

@ -3,7 +3,8 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<style> <meta http-equiv="content-security-policy" content="default-src 'none'; img-src 'self'; style-src 'nonce-{{ csp }}';">
<style nonce="{{ csp }}">
:root { :root {
--link-color: #42a5d7; --link-color: #42a5d7;
--padding-size: 0.5rem; --padding-size: 0.5rem;
@ -41,6 +42,8 @@
padding: 0; padding: 0;
} }
#tripcode { #tripcode {
background-color: {{ user.tripcode.background_color }};
color: {{ user.tripcode.foreground_color }};
cursor: pointer; cursor: pointer;
} }
.x { .x {
@ -230,11 +233,11 @@
<input id="password-toggle" name="set-tripcode" type="checkbox" accesskey="s"> <input id="password-toggle" name="set-tripcode" type="checkbox" accesskey="s">
<input id="cleared-toggle" name="clear-tripcode" type="checkbox"{% if user.tripcode != none %} accesskey="c"{% endif %}> <input id="cleared-toggle" name="clear-tripcode" type="checkbox"{% if user.tripcode != none %} accesskey="c"{% endif %}>
<div id="password-column"> <div id="password-column">
{% if user.tripcode == none %} {% if user.tripcode is none %}
<span class="tripcode">(no tripcode)</span> <span class="tripcode">(no tripcode)</span>
<label for="password-toggle" class="show-password pseudolink">set</label> <label for="password-toggle" class="show-password pseudolink">set</label>
{% else %} {% else %}
<label id="tripcode" for="password-toggle" class="show-password tripcode" style="background-color:{{ user.tripcode.background_color }};color:{{ user.tripcode.foreground_color }};">{{ user.tripcode.digest }}</label> <label id="tripcode" for="password-toggle" class="show-password tripcode">{{ user.tripcode.digest }}</label>
<label id="show-cleared" for="cleared-toggle" class="pseudolink x">&cross;</label> <label id="show-cleared" for="cleared-toggle" class="pseudolink x">&cross;</label>
<div id="cleared" class="tripcode">(cleared)</div> <div id="cleared" class="tripcode">(cleared)</div>
<label id="hide-cleared" for="cleared-toggle" class="pseudolink">undo</label> <label id="hide-cleared" for="cleared-toggle" class="pseudolink">undo</label>

ファイルの表示

@ -4,9 +4,10 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="content-security-policy" content="default-src 'none'; style-src 'nonce-{{ csp }}';">
<meta http-equiv="refresh" content="4"> <meta http-equiv="refresh" content="4">
<meta http-equiv="refresh" content="5; url={{ url_for('nojs_chat_messages_redirect', token=user.token) }}"> <meta http-equiv="refresh" content="5; url={{ url_for('nojs_chat_messages_redirect', token=user.token) }}">
<style> <style nonce="{{ csp }}">
html { html {
height: 100%; height: 100%;
} }
@ -128,6 +129,20 @@
font-size: 9pt; font-size: 9pt;
cursor: default; cursor: default;
} }
{% for token in messages | map(attribute='token') | list | unique %}
{% with user = users_by_token[token] %}
[data-token-hash="{{ user.token_hash }}"] > .chat-message__name {
color: {{ user.color }};
}
{% if user.tripcode %}
[data-token-hash="{{ user.token_hash }}"] > .tripcode {
background-color: {{ user.tripcode.background_color }};
color: {{ user.tripcode.foreground_color }};
}
{% endif %}
{% endwith %}
{% endfor %}
</style> </style>
</head> </head>
<body> <body>

ファイルの表示

@ -4,8 +4,9 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="content-security-policy" content="default-src 'none'; style-src 'nonce-{{ csp }}';">
<meta http-equiv="refresh" content="6"> <meta http-equiv="refresh" content="6">
<style> <style nonce="{{ csp }}">
html { html {
min-height: 100%; min-height: 100%;
} }
@ -80,6 +81,18 @@
font-size: 9pt; font-size: 9pt;
cursor: default; cursor: default;
} }
{% for user in users_watching + users_notwatching %}
[data-token-hash="{{ user.token_hash }}"] > .user__name {
color: {{ user.color }};
}
{% if user.tripcode %}
[data-token-hash="{{ user.token_hash }}"] > .tripcode {
background-color: {{ user.tripcode.background_color }};
color: {{ user.tripcode.foreground_color }};
}
{% endif %}
{% endfor %}
</style> </style>
</head> </head>
<body> <body>
@ -93,7 +106,7 @@
<h5>Watching ({{ users_watching | length }})</h5> <h5>Watching ({{ users_watching | length }})</h5>
<ul> <ul>
{% for user_listed in users_watching %} {% for user_listed in users_watching %}
<li class="user"> <li class="user" data-token-hash="{{ user.token_hash }}">
{{- appearance(user_listed, insignia_class='user__insignia', name_class='user__name', tag_class='user__name__tag') -}} {{- appearance(user_listed, insignia_class='user__insignia', name_class='user__name', tag_class='user__name__tag') -}}
{%- if user.token == user_listed.token %} (You){% endif -%} {%- if user.token == user_listed.token %} (You){% endif -%}
</li> </li>
@ -103,7 +116,7 @@
<h5>Not watching ({{ users_notwatching | length }})</h5> <h5>Not watching ({{ users_notwatching | length }})</h5>
<ul> <ul>
{% for user_listed in users_notwatching %} {% for user_listed in users_notwatching %}
<li class="user"> <li class="user" data-token-hash="{{ user.token_hash }}">
{{- appearance(user_listed, insignia_class='user__insignia', name_class='user__name', tag_class='user__name__tag') -}} {{- appearance(user_listed, insignia_class='user__insignia', name_class='user__name', tag_class='user__name__tag') -}}
{%- if user.token == user_listed.token %} (You){% endif -%} {%- if user.token == user_listed.token %} (You){% endif -%}
</li> </li>

ファイルの表示

@ -3,8 +3,9 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="content-security-policy" content="default-src 'none'; style-src 'nonce-{{ csp }}';">
<meta http-equiv="refresh" content="6"> <meta http-equiv="refresh" content="6">
<style> <style nonce="{{ csp }}">
body { body {
overflow-y: auto; overflow-y: auto;
margin: 0.75ch 1.25ch; margin: 0.75ch 1.25ch;

ファイルの表示

@ -3,7 +3,8 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<style> <meta http-equiv="content-security-policy" content="default-src 'none'; media-src 'self'; style-src 'nonce-{{ csp }}';">
<style nonce="{{ csp }}">
html { html {
height: 100%; height: 100%;
} }

7
anonstream/utils/security.py ノーマルファイル
ファイルの表示

@ -0,0 +1,7 @@
import secrets
def generate_csp():
'''
Generate a random Content Secuity Policy nonce.
'''
return secrets.token_urlsafe(16)