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.user import watched
from anonstream.routes.wrappers import with_user_from, auth_required
from anonstream.utils.security import generate_csp
@current_app.route('/')
@with_user_from(request)
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')
@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.user import get_default_name
from anonstream.utils.chat import generate_nonce
from anonstream.utils.security import generate_csp
from anonstream.utils.user import concatenate_for_notice
CONFIG = current_app.config
@ -18,6 +19,7 @@ USERS_BY_TOKEN = current_app.users_by_token
async def nojs_stream(user):
return await render_template(
'nojs_stream.html',
csp=generate_csp(),
user=user,
)
@ -28,6 +30,7 @@ async def nojs_info(user):
uptime, viewership = get_stream_uptime_and_viewership()
return await render_template(
'nojs_info.html',
csp=generate_csp(),
user=user,
viewership=viewership,
uptime=uptime,
@ -40,6 +43,7 @@ async def nojs_info(user):
async def nojs_chat_messages(user):
return await render_template_with_etag(
'nojs_chat_messages.html',
{'csp': generate_csp()},
user=user,
users_by_token=USERS_BY_TOKEN,
messages=get_scrollback(current_app.messages),
@ -58,6 +62,7 @@ async def nojs_chat_users(user):
users_by_presence = get_users_by_presence()
return await render_template_with_etag(
'nojs_chat_users.html',
{'csp': generate_csp()},
user=user,
get_default_name=get_default_name,
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'
return await render_template(
'nojs_chat_form.html',
csp=generate_csp(),
user=user,
state=state,
prefer_chat_form=prefer_chat_form,

ファイルの表示

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

ファイルの表示

@ -2,10 +2,10 @@
const TOKEN = document.body.dataset.token;
const TOKEN_HASH = document.body.dataset.tokenHash;
/* Content Security Policy nonce */
const CSP = document.body.dataset.csp;
/* 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_info = '<div id="info_js" data-js="true"></div>';
const jsmarkup_info_float = '<aside id="info_js__float"></aside>';
@ -57,18 +57,24 @@ const jsmarkup_chat_form = `\
</div>
</form>`;
const insert_jsmarkup = () => {jsmarkup_info_float_viewership
const insert_jsmarkup = () => {
if (document.getElementById("style-color") === null) {
const parent = document.head;
parent.insertAdjacentHTML("beforeend", jsmarkup_style_color);
const style_color = document.createElement("style");
style_color.id = "style-color";
style_color.nonce = CSP;
document.head.insertAdjacentElement("beforeend", style_color);
}
if (document.getElementById("style-tripcode-display") === null) {
const parent = document.head;
parent.insertAdjacentHTML("beforeend", jsmarkup_style_tripcode_display);
const style_tripcode_display = document.createElement("style");
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) {
const parent = document.head;
parent.insertAdjacentHTML("beforeend", jsmarkup_style_tripcode_colors);
const style_tripcode_colors = document.createElement("style");
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) {
const parent = document.getElementById("stream");

ファイルの表示

@ -3,9 +3,10 @@
<head>
<meta charset="utf-8">
<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">
</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">
<noscript><iframe id="stream_nojs" name="stream_nojs" src="{{ url_for('nojs_stream', token=user.token) }}"></iframe></noscript>
</article>

ファイルの表示

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

ファイルの表示

@ -3,7 +3,8 @@
<head>
<meta charset="utf-8">
<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 {
--link-color: #42a5d7;
--padding-size: 0.5rem;
@ -41,6 +42,8 @@
padding: 0;
}
#tripcode {
background-color: {{ user.tripcode.background_color }};
color: {{ user.tripcode.foreground_color }};
cursor: pointer;
}
.x {
@ -230,11 +233,11 @@
<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 %}>
<div id="password-column">
{% if user.tripcode == none %}
{% if user.tripcode is none %}
<span class="tripcode">(no tripcode)</span>
<label for="password-toggle" class="show-password pseudolink">set</label>
{% 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>
<div id="cleared" class="tripcode">(cleared)</div>
<label id="hide-cleared" for="cleared-toggle" class="pseudolink">undo</label>

ファイルの表示

@ -4,9 +4,10 @@
<head>
<meta charset="utf-8">
<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="5; url={{ url_for('nojs_chat_messages_redirect', token=user.token) }}">
<style>
<style nonce="{{ csp }}">
html {
height: 100%;
}
@ -128,6 +129,20 @@
font-size: 9pt;
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>
</head>
<body>

ファイルの表示

@ -4,8 +4,9 @@
<head>
<meta charset="utf-8">
<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">
<style>
<style nonce="{{ csp }}">
html {
min-height: 100%;
}
@ -80,6 +81,18 @@
font-size: 9pt;
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>
</head>
<body>
@ -93,7 +106,7 @@
<h5>Watching ({{ users_watching | length }})</h5>
<ul>
{% 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') -}}
{%- if user.token == user_listed.token %} (You){% endif -%}
</li>
@ -103,7 +116,7 @@
<h5>Not watching ({{ users_notwatching | length }})</h5>
<ul>
{% 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') -}}
{%- if user.token == user_listed.token %} (You){% endif -%}
</li>

ファイルの表示

@ -3,8 +3,9 @@
<head>
<meta charset="utf-8">
<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">
<style>
<style nonce="{{ csp }}">
body {
overflow-y: auto;
margin: 0.75ch 1.25ch;

ファイルの表示

@ -3,7 +3,8 @@
<head>
<meta charset="utf-8">
<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 {
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)