Skip to content

Commit

Permalink
Registration issues (#612)
Browse files Browse the repository at this point in the history
* update devcontainer mysql-apt-config

update mysql-apt-config version to latest (0.8.32), the previous (0.8.16) was no longer building properly

* fix email pattern

* fix for motto pattern

needed to double-escape

* simplify motto pattern

This allows basically anything, as our users wanted emojis, and various punctuation allowed for their mottos.

Have tested against a number of XSS and malicious inputs, without any ill effects so far.... ex `<script>alert('XSS')</script>`

* consolidate avatar saving and validation

consolidate the avatar saving code in XSSImageCheck
- Box, Team, User all had their own implementations, combined these into one `save_avatar`
- new `avatar_validation` function

Registration:
- check avatar validation as part of form_validation function. Previously if a user provided a bad image the user/team would be created but then it would fail at avatar creation/saving....leaving them in a bad state and unable to play
  • Loading branch information
bmartin5692 committed Jul 12, 2024
1 parent 93217b1 commit 74f4c17
Show file tree
Hide file tree
Showing 8 changed files with 83 additions and 107 deletions.
4 changes: 2 additions & 2 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
FROM python:3.8

ADD [ "https://dev.mysql.com/get/mysql-apt-config_0.8.16-1_all.deb", "/" ]
ADD [ "https://dev.mysql.com/get/mysql-apt-config_0.8.32-1_all.deb", "/" ]

RUN apt-get -qq update \
&& export DEBIAN_FRONTEND=noninteractive \
Expand All @@ -12,7 +12,7 @@ RUN apt-get -qq update \
zlib1g-dev \
python3-pycurl \
# MySQL Tools
&& dpkg -i /mysql-apt-config_0.8.16-1_all.deb \
&& dpkg -i /mysql-apt-config_0.8.32-1_all.deb \
&& apt-get -qq update \
&& apt-get -qq install -y --no-install-recommends \
mysql-shell \
Expand Down
11 changes: 8 additions & 3 deletions handlers/PublicHandlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@
send_user_registered_webhook,
send_user_validated_webhook,
)
from libs.XSSImageCheck import filter_avatars
from libs.XSSImageCheck import (
filter_avatars,
avatar_validation,
)
from models import azuread_app
from models.EmailToken import EmailToken
from models.GameLevel import GameLevel
Expand Down Expand Up @@ -488,7 +491,7 @@ def form_validation(self):
self.get_argument("motto", None)
and bool(
re.match(
r"^[0-9A-Za-z _\-\.%s]{,32}$" % unicodewd,
r"^[\s\S]{0,32}$",
self.get_argument("motto", ""),
re.UNICODE,
)
Expand All @@ -510,7 +513,9 @@ def form_validation(self):
raise ValidationError("Passwords do not match")
if self.config.use_recaptcha and self.verify_recaptcha() is False:
raise ValidationError("Invalid reCAPTCHA")

if hasattr(self.request, "files") and "avatar" in self.request.files:
avatar_validation(self.request.files["avatar"][0]["body"])

def verify_recaptcha(self):
"""Checks recaptcha"""
recaptcha_response = self.get_argument("g-recaptcha-response", None)
Expand Down
50 changes: 50 additions & 0 deletions libs/XSSImageCheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,19 @@
"""

import io
import os
from random import randint, sample
from string import printable
from pathlib import Path
import imghdr

from PIL import Image
from resizeimage import resizeimage
from tornado.options import options

from libs.ValidationError import ValidationError

MAX_AVATAR_SIZE = 1024 * 1024
MIN_AVATAR_SIZE = 64
IMG_FORMATS = ["png", "jpeg", "jpg", "gif", "bmp"]
Expand Down Expand Up @@ -87,3 +94,46 @@ def existing_avatars(dir):
if user.avatar is not None:
avatars.append(user.avatar)
return avatars


def avatar_validation(image_data) -> str:
"""Avatar validation check
Returns image extension as str if checks pass
"""
if MIN_AVATAR_SIZE < len(image_data) < MAX_AVATAR_SIZE:
ext = imghdr.what("", h=image_data)
if ext in IMG_FORMATS and not is_xss_image(image_data):
return ext
else:
raise ValidationError(
"Invalid image format, avatar must be: %s"
% (", ".join(IMG_FORMATS))
)

else:
raise ValidationError(
"The image is too large must be %d - %d bytes"
% (MIN_AVATAR_SIZE, MAX_AVATAR_SIZE)
)

def save_avatar(path: str, image_data: bytes) -> str:
"""
Save avatar image to path
Returns image path without avatar_dir
"""
try:
base_path = Path(path)
image_path = os.path.join(options.avatar_dir, base_path)

if os.path.exists(image_path):
os.unlink(image_path)

image = Image.open(io.BytesIO(image_data))
cover = resizeimage.resize_cover(image, [500, 250])
cover.save(image_path, image.format)
return str(base_path)

except Exception as e:
raise ValidationError(e)
38 changes: 11 additions & 27 deletions models/Box.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,24 @@

import binascii
import enum
import imghdr
import io
import os
import xml.etree.cElementTree as ET
from collections import OrderedDict
from os import urandom
from uuid import uuid4

from PIL import Image
from resizeimage import resizeimage
from sqlalchemy import Column, ForeignKey
from sqlalchemy.orm import backref, relationship
from sqlalchemy.types import Boolean, Enum, Integer, String, Unicode
from tornado.options import options

from libs.StringCoding import decode, encode
from libs.ValidationError import ValidationError
from libs.XSSImageCheck import get_new_avatar, is_xss_image
from libs.XSSImageCheck import (
get_new_avatar,
avatar_validation,
save_avatar
)
from models import dbsession
from models.BaseModels import DatabaseObject
from models.Category import Category
Expand Down Expand Up @@ -352,28 +352,12 @@ def avatar(self, image_data):

if self.uuid is None:
self.uuid = str(uuid4())
if len(image_data) < (1024 * 1024):
ext = imghdr.what("", h=image_data)
if ext in ["png", "jpeg", "gif", "bmp"] and not is_xss_image(image_data):
try:
if self._avatar is not None:
current_image_path = os.path.join(options.avatar_dir, avatar_path, self._avatar) if avatar_path == "upload" else os.path.join(options.avatar_dir, avatar_path)
if os.path.exists(current_image_path):
os.unlink(current_image_path)

new_image_path = os.path.join(avatar_path, f"{self.uuid}.{ext}") if avatar_path == "upload" else avatar_path
image = Image.open(io.BytesIO(image_data))
cover = resizeimage.resize_cover(image, [500, 250])
cover.save(os.path.join(options.avatar_dir,new_image_path), image.format)
self._avatar = new_image_path
except Exception as e:
raise ValidationError(e)
else:
raise ValidationError(
"Invalid image format, avatar must be: .png .jpeg .gif or .bmp"
)
else:
raise ValidationError("The image is too large")

if avatar_path == "upload":
os.path.join("upload", f"{self.uuid}.{ext}")

ext = avatar_validation(image_data)
self._avatar = save_avatar(avatar_path)

@property
def ipv4s(self):
Expand Down
42 changes: 5 additions & 37 deletions models/Team.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,13 @@
# pylint: disable=no-member


import imghdr
import io
import os
import xml.etree.cElementTree as ET
from builtins import str
from datetime import datetime
from random import randint
from uuid import uuid4

from PIL import Image
from resizeimage import resizeimage
from sqlalchemy import Column, desc
from sqlalchemy.orm import backref, relationship
from sqlalchemy.types import Integer, String, Unicode
Expand All @@ -41,11 +37,9 @@
from libs.StringCoding import encode
from libs.ValidationError import ValidationError
from libs.XSSImageCheck import (
IMG_FORMATS,
MAX_AVATAR_SIZE,
MIN_AVATAR_SIZE,
get_new_avatar,
is_xss_image,
get_new_avatar,
avatar_validation,
save_avatar,
)
from models import dbsession
from models.BaseModels import DatabaseObject
Expand Down Expand Up @@ -263,34 +257,8 @@ def avatar(self):

@avatar.setter
def avatar(self, image_data):
if MIN_AVATAR_SIZE < len(image_data) < MAX_AVATAR_SIZE:
ext = imghdr.what("", h=image_data)
if ext in IMG_FORMATS and not is_xss_image(image_data):
try:
if self._avatar is not None and os.path.exists(
options.avatar_dir + "/upload/" + self._avatar
):
os.unlink(options.avatar_dir + "/upload/" + self._avatar)
file_path = str(
options.avatar_dir + "/upload/" + self.uuid + "." + ext
)
image = Image.open(io.BytesIO(image_data))
cover = resizeimage.resize_cover(image, [500, 250])
cover.save(file_path, image.format)
self._avatar = "upload/" + self.uuid + "." + ext
except Exception as e:
raise ValidationError(e)

else:
raise ValidationError(
"Invalid image format, avatar must be: %s"
% (", ".join(IMG_FORMATS))
)
else:
raise ValidationError(
"The image is too large must be %d - %d bytes"
% (MIN_AVATAR_SIZE, MAX_AVATAR_SIZE)
)
ext = avatar_validation(image_data)
self._avatar = save_avatar(os.path.join("upload", f"{self.uuid}.{ext}"),image_data)

@property
def levels(self):
Expand Down
41 changes: 5 additions & 36 deletions models/User.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@
"""


import imghdr
import io
import os
import random
import string
Expand All @@ -39,8 +37,6 @@

from past.builtins import basestring
from pbkdf2 import PBKDF2
from PIL import Image
from resizeimage import resizeimage
from sqlalchemy import Column, ForeignKey, desc, func
from sqlalchemy.orm import backref, relationship, synonym
from sqlalchemy.types import Boolean, DateTime, Integer, String, Unicode
Expand All @@ -50,12 +46,10 @@
from libs.ValidationError import ValidationError
from libs.WebhookHelpers import send_user_validated_webhook
from libs.XSSImageCheck import (
IMG_FORMATS,
MAX_AVATAR_SIZE,
MIN_AVATAR_SIZE,
avatar_validation,
save_avatar,
default_avatar,
get_new_avatar,
is_xss_image,
get_new_avatar,
)
from models import dbsession
from models.BaseModels import DatabaseObject
Expand Down Expand Up @@ -354,33 +348,8 @@ def avatar(self):

@avatar.setter
def avatar(self, image_data):
if MIN_AVATAR_SIZE < len(image_data) < MAX_AVATAR_SIZE:
ext = imghdr.what("", h=image_data)
if ext in IMG_FORMATS and not is_xss_image(image_data):
try:
if self._avatar is not None and os.path.exists(
options.avatar_dir + "/upload/" + self._avatar
):
os.unlink(options.avatar_dir + "/upload/" + self._avatar)
file_path = str(
options.avatar_dir + "/upload/" + self.uuid + "." + ext
)
image = Image.open(io.BytesIO(image_data))
cover = resizeimage.resize_cover(image, [500, 250])
cover.save(file_path, image.format)
self._avatar = "upload/" + self.uuid + "." + ext
except Exception as e:
raise ValidationError(e)
else:
raise ValidationError(
"Invalid image format, avatar must be: %s"
% (", ".join(IMG_FORMATS))
)
else:
raise ValidationError(
"The image is too large must be %d - %d bytes"
% (MIN_AVATAR_SIZE, MAX_AVATAR_SIZE)
)
ext = avatar_validation(image_data)
self._avatar = save_avatar(os.path.join("upload", f"{self.uuid}.{ext}"),image_data)

def has_item(self, item_name):
"""Check to see if a team has purchased an item"""
Expand Down
2 changes: 1 addition & 1 deletion static/js/pages/public/registration.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,6 @@ $(document).ready(function() {
let unicodewd = "ªµºÀ-ÖØ-öø-ˁˆ-ˑˠ-ˤˬˮͰ-ʹͶͷͺ-ͽͿΆΈ-ΊΌΎ-ΡΣ-ϵϷ-ҁҊ-ԯԱ-Ֆՙՠ-ֈא-תׯ-ײؠ-يٮٯٱ-ۓەۥۦۮۯۺ-ۼۿܐܒ-ܯݍ-ޥޱߊ-ߪߴߵߺࠀ-ࠕࠚࠤࠨࡀ-ࡘࡠ-ࡪࡰ-ࢇࢉ-ࢎࢠ-ࣉऄ-हऽॐक़-ॡॱ-ঀঅ-ঌএঐও-নপ-রলশ-হঽৎড়ঢ়য়-ৡৰৱৼਅ-ਊਏਐਓ-ਨਪ-ਰਲਲ਼ਵਸ਼ਸਹਖ਼-ੜਫ਼ੲ-ੴઅ-ઍએ-ઑઓ-નપ-રલળવ-હઽૐૠૡૹଅ-ଌଏଐଓ-ନପ-ରଲଳଵ-ହଽଡ଼ଢ଼ୟ-ୡୱஃஅ-ஊஎ-ஐஒ-கஙசஜஞடணதந-பம-ஹௐఅ-ఌఎ-ఐఒ-నప-హఽౘ-ౚౝౠౡಀಅ-ಌಎ-ಐಒ-ನಪ-ಳವ-ಹಽೝೞೠೡೱೲഄ-ഌഎ-ഐഒ-ഺഽൎൔ-ൖൟ-ൡൺ-ൿඅ-ඖක-නඳ-රලව-ෆก-ะาำเ-ๆກຂຄຆ-ຊຌ-ຣລວ-ະາຳຽເ-ໄໆໜ-ໟༀཀ-ཇཉ-ཬྈ-ྌက-ဪဿၐ-ၕၚ-ၝၡၥၦၮ-ၰၵ-ႁႎႠ-ჅჇჍა-ჺჼ-ቈቊ-ቍቐ-ቖቘቚ-ቝበ-ኈኊ-ኍነ-ኰኲ-ኵኸ-ኾዀዂ-ዅወ-ዖዘ-ጐጒ-ጕጘ-ፚᎀ-ᎏᎠ-Ᏽᏸ-ᏽᐁ-ᙬᙯ-ᙿᚁ-ᚚᚠ-ᛪᛱ-ᛸᜀ-ᜑᜟ-ᜱᝀ-ᝑᝠ-ᝬᝮ-ᝰក-ឳៗៜᠠ-ᡸᢀ-ᢄᢇ-ᢨᢪᢰ-ᣵᤀ-ᤞᥐ-ᥭᥰ-ᥴᦀ-ᦫᦰ-ᧉᨀ-ᨖᨠ-ᩔᪧᬅ-ᬳᭅ-ᭌᮃ-ᮠᮮᮯᮺ-ᯥᰀ-ᰣᱍ-ᱏᱚ-ᱽᲀ-ᲈᲐ-ᲺᲽ-Ჿᳩ-ᳬᳮ-ᳳᳵᳶᳺᴀ-ᶿḀ-ἕἘ-Ἕἠ-ὅὈ-Ὅὐ-ὗὙὛὝὟ-ώᾀ-ᾴᾶ-ᾼιῂ-ῄῆ-ῌῐ-ΐῖ-Ίῠ-Ῥῲ-ῴῶ-ῼⁱⁿₐ-ₜℂℇℊ-ℓℕℙ-ℝℤΩℨK-ℭℯ-ℹℼ-ℿⅅ-ⅉⅎↃↄⰀ-ⳤⳫ-ⳮⳲⳳⴀ-ⴥⴧⴭⴰ-ⵧⵯⶀ-ⶖⶠ-ⶦⶨ-ⶮⶰ-ⶶⶸ-ⶾⷀ-ⷆⷈ-ⷎⷐ-ⷖⷘ-ⷞⸯ々〆〱-〵〻〼ぁ-ゖゝ-ゟァ-ヺー-ヿㄅ-ㄯㄱ-ㆎㆠ-ㆿㇰ-ㇿ㐀-䶿一-ꒌꓐ-ꓽꔀ-ꘌꘐ-ꘟꘪꘫꙀ-ꙮꙿ-ꚝꚠ-ꛥꜗ-ꜟꜢ-ꞈꞋ-ꟊꟐꟑꟓꟕ-ꟙꟲ-ꠁꠃ-ꠅꠇ-ꠊꠌ-ꠢꡀ-ꡳꢂ-ꢳꣲ-ꣷꣻꣽꣾꤊ-ꤥꤰ-ꥆꥠ-ꥼꦄ-ꦲꧏꧠ-ꧤꧦ-ꧯꧺ-ꧾꨀ-ꨨꩀ-ꩂꩄ-ꩋꩠ-ꩶꩺꩾ-ꪯꪱꪵꪶꪹ-ꪽꫀꫂꫛ-ꫝꫠ-ꫪꫲ-ꫴꬁ-ꬆꬉ-ꬎꬑ-ꬖꬠ-ꬦꬨ-ꬮꬰ-ꭚꭜ-ꭩꭰ-ꯢ가-힣ힰ-ퟆퟋ-ퟻ豈-舘並-龎ff-stﬓ-ﬗיִײַ-ﬨשׁ-זּטּ-לּמּנּסּףּפּצּ-ﮱﯓ-ﴽﵐ-ﶏﶒ-ﷇﷰ-ﷻﹰ-ﹴﹶ-ﻼA-Za-zヲ-하-ᅦᅧ-ᅬᅭ-ᅲᅳ-ᅵ";

$("#playername").attr("pattern", "^[0-9A-Za-z " + unicodewd + "]{3,64}$");
$("#motto").attr("pattern", "^[0-9A-Za-z _\-\." + unicodewd + "]{,32}$");
$("#motto").attr("pattern", "^[\\s\\S]{0,32}$");

});
2 changes: 1 addition & 1 deletion templates/public/registration.html
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ <h4 class="alert-heading">{{ _("ERROR") }}</h4>
</label>
<div class="controls">
<input required id="email" name="email" maxlength="64" minlength="3" placeholder="{{ _('Email') }}" type="text"
rel="popover" pattern="[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
rel="popover" pattern="[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$"
data-original-title="{{_('Player Email')}}"
data-content="{{_('Your email, visible to game administrators.')}}" />
</div>
Expand Down

0 comments on commit 74f4c17

Please sign in to comment.