Compare commits

..

6 Commits

Author SHA1 Message Date
rubenwardy
5e60cb83de Add XSS strings to test data 2020-01-22 23:45:40 +00:00
rubenwardy
595d6ea3b6 Use server-side markdown renderer in WYSIWYG preview
Fixes #117
2020-01-22 23:10:06 +00:00
rubenwardy
71fa62fd6a Update EasyMDE 2020-01-22 22:47:24 +00:00
rubenwardy
be5bb11fe3 Add data* to docker ignore 2020-01-22 22:11:01 +00:00
rubenwardy
981ae74e5c Improve markdown escaping
Fixes #118
2020-01-22 22:10:02 +00:00
rubenwardy
2b66193969 Fix version being styled differently 2020-01-21 23:42:41 +00:00
13 changed files with 166 additions and 21 deletions

View File

@@ -1,5 +1,5 @@
.git
data
data*
uploads
*.pyc
__pycache__

View File

@@ -20,7 +20,6 @@ from flask_user import *
from flask_gravatar import Gravatar
import flask_menu as menu
from flask_mail import Mail
from flaskext.markdown import Markdown
from flask_github import GitHub
from flask_wtf.csrf import CsrfProtect
from flask_flatpages import FlatPages
@@ -35,7 +34,6 @@ app.config.from_pyfile(os.environ["FLASK_CONFIG"])
r = redis.Redis.from_url(app.config["REDIS_URL"])
menu.Menu(app=app)
markdown = Markdown(app, extensions=["fenced_code"], safe_mode=True, output_format="html5")
github = GitHub(app)
csrf = CsrfProtect(app)
mail = Mail(app)
@@ -59,9 +57,13 @@ if not app.debug and app.config["MAIL_UTILS_ERROR_SEND_TO"]:
register_mail_error_handler(app, mail)
@babel.localeselector
def get_locale():
return request.accept_languages.best_match(app.config['LANGUAGES'].keys())
from .markdown import init_app
init_app(app)
# @babel.localeselector
# def get_locale():
# return request.accept_languages.best_match(app.config['LANGUAGES'].keys())
from . import models, tasks, template_filters

View File

@@ -19,8 +19,10 @@ from flask import *
from flask_user import *
from . import bp
from .auth import is_api_authd
from app import csrf
from app.models import *
from app.utils import is_package_page
from app.markdown import render_markdown
from app.querybuilder import QueryBuilder
@bp.route("/api/packages/")
@@ -107,3 +109,9 @@ def whoami(token):
return jsonify({ "is_authenticated": False, "username": None })
else:
return jsonify({ "is_authenticated": True, "username": token.owner.username })
@bp.route("/api/markdown/", methods=["POST"])
@csrf.exempt
def clean_markdown():
return render_markdown(request.data.decode("utf-8"))

View File

@@ -18,7 +18,7 @@
from flask import *
from flask_user import *
from flask_login import login_user, logout_user
from app import markdown
from app.markdown import render_markdown
from . import bp
from app.models import *
from flask_wtf import FlaskForm
@@ -153,7 +153,7 @@ def send_email(username):
form = SendEmailForm(request.form)
if form.validate_on_submit():
text = form.text.data
html = markdown(text)
html = render_markdown(text)
task = sendEmailRaw.delay([user.email], form.subject.data, text, html)
return redirect(url_for("tasks.check", id=task.id, r=next_url))

View File

@@ -304,6 +304,38 @@ No warranty is provided, express or implied, for any part of the project.
game1.desc = """
As seen on the Capture the Flag server (minetest.rubenwardy.com:30000)
` `[`javascript:/*--></title></style></textarea></script></xmp><svg/onload='+/"/+/onmouseover=1/+/`](javascript:/*--%3E%3C/title%3E%3C/style%3E%3C/textarea%3E%3C/script%3E%3C/xmp%3E%3Csvg/onload='+/%22/+/onmouseover=1/+/)`[*/[]/+alert(1)//'>`
<IMG SRC="javascript:alert('XSS');">
<IMG SRC=javascript:alert(&amp;quot;XSS&amp;quot;)>
``<IMG SRC=`javascript:alert("RSnake says, 'XSS'")`>``
\<a onmouseover="alert(document.cookie)"\>xxs link\</a\>
\<a onmouseover=alert(document.cookie)\>xxs link\</a\>
<IMG SRC=javascript:alert(String.fromCharCode(88,83,83))>
<script>alert("hello");</script>
<SCRIPT SRC=`[`http://xss.rocks/xss.js></SCRIPT>`](http://xss.rocks/xss.js%3E%3C/SCRIPT%3E)`;`
`<IMG \"\"\">`
<SCRIPT>
alert("XSS")
</SCRIPT>
<IMG SRC= onmouseover="alert('xxs')">
<img src=x onerror="&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041">
"\>
Uses the CTF PvP Engine.
"""

63
app/markdown.py Normal file
View File

@@ -0,0 +1,63 @@
import bleach
from markdown import Markdown
from flask import Markup
# Whitelist source: MIT
#
# https://github.com/Wenzil/mdx_bleach/blob/master/mdx_bleach/whitelist.py
"""
Default whitelist of allowed HTML tags. Any other HTML tags will be escaped or
stripped from the text. This applies to the html output that Markdown produces.
"""
ALLOWED_TAGS = [
'ul',
'ol',
'li',
'p',
'pre',
'code',
'blockquote',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'hr',
'br',
'strong',
'em',
'a',
'img'
]
"""
Default whitelist of attributes. It allows the href and title attributes for <a>
tags and the src, title and alt attributes for <img> tags. Any other attribute
will be stripped from its tag.
"""
ALLOWED_ATTRIBUTES = {
'a': ['href', 'title'],
'img': ['src', 'title', 'alt']
}
"""
If you allow tags that have attributes containing a URI value
(like the href attribute of an anchor tag,) you may want to adapt
the accepted protocols. The default list only allows http, https and mailto.
"""
ALLOWED_PROTOCOLS = ['http', 'https', 'mailto']
md = Markdown(extensions=["fenced_code"], output_format="html5")
def render_markdown(source):
return bleach.clean(md.convert(source), \
tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES, \
styles=[], protocols=ALLOWED_PROTOCOLS)
def init_app(app):
@app.template_filter()
def markdown(source):
return Markup(render_markdown(source))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,34 @@
$("textarea.markdown").each(function() {
async function render(plainText, preview) {
const response = await fetch(new Request("/api/markdown/", {
method: "POST",
credentials: "same-origin",
body: plainText,
headers: {
"Accept": "text/html; charset=UTF-8",
},
}));
preview.innerHTML = await response.text();
}
let timeout_id = null;
new EasyMDE({
element: this,
hideIcons: ["image"],
forceSync: true,
previewRender: (plainText, preview) => {
if (timeout_id) {
clearTimeout(timeout_id);
}
timeout_id = setTimeout(() => {
render(plainText, preview);
timeout_id = null;
}, 500);
return preview.innerHTML;
}
});
})

View File

@@ -148,12 +148,9 @@
<script src="/static/bootstrap.min.js"></script>
<script src="/static/easymde.min.js"></script>
<link rel="stylesheet" type="text/css" href="/static/easymde.min.css">
<script>
$("textarea.markdown").each(function() {
new EasyMDE({ element: this, hideIcons: ["image"], forceSync: true });
})
</script>
<link href="/static/fa/css/all.css" rel="stylesheet">
<script src="/static/markdowntextarea.js"></script>
{% block scriptextra %}{% endblock %}
</body>
</html>

View File

@@ -84,7 +84,7 @@
{% endif %}
</div>
{% if release and (release.min_rel or release.max_rel) %}
<div class="text-secondary col-md-auto">
<div class="btn col-md-auto">
<img src="https://www.minetest.net/media/icon.svg" style="max-height: 1.2em;">
<span class="count">
{% if release.min_rel and release.max_rel %}
@@ -364,7 +364,7 @@
</ul>
</div>
<div class="card my-4"">
<div class="card my-4">
<div class="card-header">
{% if package.approved and package.checkPerm(current_user, "CREATE_THREAD") %}
<div class="btn-group float-right">

View File

@@ -60,7 +60,7 @@ Topics to be Added
{% set perc = 100 * (total - topic_count) / total %}
<div class="progress-bar bg-success" role="progressbar"
style="width: {{ perc }}%" aria-valuenow="{{ perc }}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
{% else %}
<p>
The forum topic crawler needs to run at least once for this section to work.

View File

@@ -2,7 +2,6 @@ Flask~=1.1
Flask-FlatPages~=0.7
Flask-Gravatar~=0.5
Flask-Login~=0.4.1
Flask-Markdown~=0.3
Flask-Menu~=0.7
Flask-Migrate~=2.3
Flask-SQLAlchemy~=2.3
@@ -11,6 +10,9 @@ Flask-Babel
GitHub-Flask~=3.2
SQLAlchemy-Searchable~=1.1
markdown ~= 3.1
bleach ~= 3.1
beautifulsoup4~=4.6
celery~=4.4
kombu~=4.6