I've written a blog site using python
and Flask
. It works a little like Reddit, but instead of having some anonymous users, there are only anonymous users. This site allows users to add posts (after being filtered, of course). I'd like feedback on a couple things:
Is there any way I can better filter user input for
XSS
? I only check if the beginning or ending of each word is<
or>
, which can probably be bypassed fairly easily.I store information about each post in a
json
file. Is this a good way to store this type of information?
As always, feedback in any other areas is accepted and considered.
File Structure
Blog
| __pycache__
| posts
...json files generated by users creating posts...
| templates
| index.html
| config.py (only thing in this file is SECRET_KEY)
| post.py
| server.py
server.py
"""
Main Module for running and managing the blog
"""
import random
import json
import os
from flask import Flask, render_template, request, flash, redirect, url_for
from config import SECRET_KEY
from post import Post
APP = Flask(__name__)
APP.secret_key = SECRET_KEY
POSTS = "posts/"
@APP.route("/", methods=["GET", "POST"])
def index():
"""
Main Page, responsible for displaying posts and
adding new posts
"""
if request.method == "POST":
content = request.form['content']
new_post = Post(generate_user(), content)
if new_post.postable():
add_post(new_post)
else:
error = ""
if new_post.contains_profanity():
error += "Your post contains profanity! "
if new_post.over_max_length():
error += "Your post is over the max length! "
if new_post.contains_html():
error += "Your post contains html! "
flash(error)
return redirect(url_for("index"))
return render_template("index.html", post=get_posts())
def get_posts() -> list:
"""
Returns all the posts created
"""
files = os.listdir(POSTS)
posts = []
for file in files:
with open(f"{POSTS}{file}", "r") as user_post:
data = json.load(user_post)
posts.append(data)
return posts
def add_post(new_post) -> None:
"""
Adds a new post to the posts folder
"""
data = {
'USER': new_post.user,
'UNIQUE ID': new_post.unique_id,
'CONTENT': new_post.content,
'DATE POSTED': new_post.date_created
}
with open(f"{POSTS}{new_post.unique_id}.json", "w") as file:
json.dump(data, file, indent=4)
def generate_user() -> str:
"""
Returns a random number for assigning to an anonymous user
when they create a post
"""
return f"Anonymous#{random.randint(1_000_000, 9_999_999)}"
if __name__ == '__main__':
APP.run(debug=True, host="0.0.0.0", port=80)
post.py
"""
This module is for the sole purpose of containing the
Post class
"""
import random
import datetime
class Post():
"""
Class for peoples posts
"""
def __init__(self, user, content):
self.user = user
self.unique_id = self.generate_unique_id()
self.content = content
self.max_characters = 5000
now = datetime.datetime.now()
end = "PM" if now.hour >= 12 else "AM"
self.date_created = f"{now.month}/{now.day}/{now.year} {now.hour}:{now.minute}:{now.second} {end}"
def generate_unique_id(self) -> int:
"""
Returns a unique id for this post
"""
return random.randint(100_000_000, 999_999_999)
def contains_profanity(self) -> bool:
"""
Returns a boolean based on if there is profanity
in the post. This checks against a VERY basic list
of profanity
"""
for word in self.content.split():
if word.lower() in ["foo", "bar", "word"]:
return True
return False
def over_max_length(self) -> bool:
"""
Returns a boolean based on if the post exceeds the
max limit allowed
"""
return len(self.content) > self.max_characters
def contains_html(self) -> bool:
"""
Returns a boolean if there is any html in the
post. This checks against a very basic list of
html, ones that are most common for XSS
"""
for word in self.content.split():
if word[0] == "<" or word[-1] == ">":
return True
return False
def postable(self) -> bool:
"""
Uses all of these class methods to determine if
this post is allowed to be posted
"""
return not self.contains_html() and \
not self.over_max_length() and \
not self.contains_profanity()
index.html
<!DOCTYPE html>
<html lang="en-US">
<head>
<title>Blog</title>
<style type="text/css">
fieldset {
border-radius: 10px;
border-width: 5px;
}
input[type=text] {
border-color: yellow;
border-radius: 5px;
width: 150px;
height: 25px;
font-size: 17px;
}
body {
background: pink;
}
textarea {
min-height: 100px;
max-width: 500px;
min-width: 500px;
max-width: 1000px;
font-size: 16px;
border-radius: 3px;
}
button[type=submit] {
margin-top: 5px;
height: 40px;
width: 120px;
border-radius: 10px;
font-size: 20px;
background-color: yellow;
color: black;
letter-spacing: 1px;
border-color: black;
}
button[type=submit]:hover {
background-color: purple;
color: white;
cursor: pointer;
}
#user { font-size: 20px; }
#date { font-size: 15px; }
#content {
margin-top: 10px;
font-size: 14px;
}
</style>
</head>
<body>
<!-- Posts Start -->
{% for p in post %}
<fieldset>
<div id="user"><b>{{ p['USER'] }} - {{ p['DATE POSTED'] }}</b></div>
<div id="content">{{ p['CONTENT'] }}</div>
</fieldset>
{% endfor %}
<!-- Posts End -->
<!-- New Post Start -->
<form action="" method="post">
<fieldset>
<h3>Create New Post</h3>
<textarea placeholder="Enter Content Here" name="content"></textarea><br>
<button type="submit"><b>Post</b></button>
</fieldset>
</form>
<!-- New Post End -->
<!-- Errors Start -->
{% for message in get_flashed_messages() %}
{{ message }}
{% endfor %}
<!-- Errors End -->
</body>
</html>
How the JSON data is stored
{
"USER": "Anonymous#8147466",
"UNIQUE ID": 766866833,
"CONTENT": "This is my post!",
"DATE POSTED": "9/27/2019 12:50:7 PM"
}
Site after above post is made
<!DOCTYPE html>
<html lang="en-US">
<head>
<title>Blog</title>
<style type="text/css">
fieldset {
border-radius: 10px;
border-width: 5px;
}
input[type=text] {
border-color: yellow;
border-radius: 5px;
width: 150px;
height: 25px;
font-size: 17px;
}
body {
background: pink;
}
textarea {
min-height: 100px;
max-width: 500px;
min-width: 500px;
max-width: 1000px;
font-size: 16px;
border-radius: 3px;
}
button[type=submit] {
margin-top: 5px;
height: 40px;
width: 120px;
border-radius: 10px;
font-size: 20px;
background-color: yellow;
color: black;
letter-spacing: 1px;
border-color: black;
}
button[type=submit]:hover {
background-color: purple;
color: white;
cursor: pointer;
}
#user { font-size: 20px; }
#date { font-size: 15px; }
#content {
margin-top: 10px;
font-size: 14px;
}
</style>
</head>
<body>
<!-- Posts Start -->
<fieldset>
<div id="user"><b>Anonymous#8147466 - 9/27/2019 12:50:7 PM</b></div>
<div id="content">This is my post!</div>
</fieldset>
<!-- Posts End -->
<!-- New Post Start -->
<form action="" method="post">
<fieldset>
<h3>Create New Post</h3>
<textarea placeholder="Enter Content Here" name="content"></textarea><br>
<button type="submit"><b>Post</b></button>
</fieldset>
</form>
<!-- New Post End -->
<!-- Errors Start -->
<!-- Errors End -->
</body>
</html>