fix: restrict web ingest to official YouTube hosts and bound submission size
The submit form's URL validation accepted any http/https host. Restrict it to an explicit allowlist of official YouTube hosts using normalized, exact hostname comparison (no substring/suffix matching), reject embedded userinfo and malformed URLs, and add server-side length bounds for the URL, submitter, note, and total request body.
This commit is contained in:
@@ -224,12 +224,36 @@ HTML_FORM = """
|
||||
</html>
|
||||
"""
|
||||
|
||||
ALLOWED_YOUTUBE_HOSTS = frozenset(
|
||||
{
|
||||
"youtube.com",
|
||||
"www.youtube.com",
|
||||
"m.youtube.com",
|
||||
"music.youtube.com",
|
||||
"youtu.be",
|
||||
}
|
||||
)
|
||||
|
||||
MAX_URL_LENGTH = 2048
|
||||
MAX_SUBMITTER_LENGTH = 200
|
||||
MAX_NOTE_LENGTH = 1000
|
||||
MAX_REQUEST_CONTENT_LENGTH = 32 * 1024
|
||||
|
||||
|
||||
def _valid_media_url(value: str) -> bool:
|
||||
if not value or len(value) > MAX_URL_LENGTH:
|
||||
return False
|
||||
try:
|
||||
parsed = urlsplit(value)
|
||||
hostname = parsed.hostname
|
||||
has_userinfo = parsed.username is not None or parsed.password is not None
|
||||
except ValueError:
|
||||
return False
|
||||
return parsed.scheme.lower() in {"http", "https"} and bool(parsed.netloc)
|
||||
if parsed.scheme.lower() not in {"http", "https"}:
|
||||
return False
|
||||
if has_userinfo or not hostname:
|
||||
return False
|
||||
return hostname.lower() in ALLOWED_YOUTUBE_HOSTS
|
||||
|
||||
|
||||
def _submission_message(result: dict[str, object]) -> str:
|
||||
@@ -277,6 +301,7 @@ def create_app(
|
||||
web_app.config.update(
|
||||
TESTING=testing,
|
||||
SECRET_KEY=secret_key or "controller-test-only-secret",
|
||||
MAX_CONTENT_LENGTH=MAX_REQUEST_CONTENT_LENGTH,
|
||||
)
|
||||
web_app.extensions["controller_service"] = service
|
||||
web_app.extensions["controller_config"] = config
|
||||
@@ -285,6 +310,11 @@ def create_app(
|
||||
def index():
|
||||
return render_template_string(HTML_FORM)
|
||||
|
||||
@web_app.errorhandler(413)
|
||||
def _submission_too_large(_error):
|
||||
flash("The submission was too large.")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
@web_app.post("/submit")
|
||||
def submit():
|
||||
url = request.form.get("url", "").strip()
|
||||
@@ -292,7 +322,16 @@ def create_app(
|
||||
note = request.form.get("note", "").strip() or None
|
||||
|
||||
if not _valid_media_url(url):
|
||||
flash("Invalid URL. Use a complete http:// or https:// address.")
|
||||
flash(
|
||||
"Invalid URL. Only YouTube links are accepted "
|
||||
"(e.g. https://www.youtube.com/... or https://youtu.be/...)."
|
||||
)
|
||||
return redirect(url_for("index"))
|
||||
if submitter is not None and len(submitter) > MAX_SUBMITTER_LENGTH:
|
||||
flash(f"Your name must be {MAX_SUBMITTER_LENGTH} characters or fewer.")
|
||||
return redirect(url_for("index"))
|
||||
if note is not None and len(note) > MAX_NOTE_LENGTH:
|
||||
flash(f"The note must be {MAX_NOTE_LENGTH} characters or fewer.")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user