Skip to content

Commit

Permalink
Add a check for files extension
Browse files Browse the repository at this point in the history
  • Loading branch information
vaamb committed Feb 2, 2025
1 parent 0efad5a commit dcacc8e
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 19 deletions.
3 changes: 3 additions & 0 deletions src/ouranos/core/config/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ def __setitem__(self, key, value):
MAX_TEXT_FILE_SIZE = 10 * 1024 * 1024
MAX_PICTURE_FILE_SIZE = 10 * 1024 * 1024

# File extensions
SUPPORTED_TEXT_EXTENSIONS = {"md", "txt"}
SUPPORTED_IMAGE_EXTENSIONS = {"gif", "jpeg", "jpg", "png", "svg", "webp"}

# Login
SESSION_FRESHNESS = 15 * 60 * 60
Expand Down
16 changes: 5 additions & 11 deletions src/ouranos/core/database/models/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@

from ouranos import current_app
from ouranos.core.config.consts import (
REGISTRATION_TOKEN_VALIDITY, TOKEN_SUBS)
REGISTRATION_TOKEN_VALIDITY, SUPPORTED_IMAGE_EXTENSIONS, TOKEN_SUBS)
from ouranos.core.database.models.abc import Base, CRUDMixin, ToDictMixin
from ouranos.core.database.models.caches import cache_users
from ouranos.core.database.models.types import PathType, UtcDateTime
from ouranos.core.database.utils import ArchiveLink
from ouranos.core.utils import humanize_list, slugify, Tokenizer
from ouranos.core.utils import check_filename, slugify, Tokenizer


argon2_hasher = PasswordHasher()

Expand Down Expand Up @@ -1401,17 +1402,10 @@ async def create(
raise WikiArticleNotFound
# Get extension info
name: str = lookup_keys["name"]
extension: str = values.pop("extension", None) or name.split(".")[-1]
extension: str = values.pop("extension")
if not extension.startswith("."):
extension = f".{extension}"
name: str = name.split(extension)[0]
lookup_keys["name"] = name
supported = {'.gif', '.jpeg', '.jpg', '.png', '.svg', '.webp'}
if extension.lower() not in supported:
raise ValueError(
f"This image format is not supported. Image format supported: "
f"{humanize_list([*supported])}."
)
check_filename(f"{name}{extension}", SUPPORTED_IMAGE_EXTENSIONS)
# Get path info
path = article.path / f"{slugify(name)}{extension}"
values["path"] = str(path)
Expand Down
14 changes: 14 additions & 0 deletions src/ouranos/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,17 @@ def check_secret_key(config: dict) -> str | None:

def slugify(s: str) -> str:
return _slugify(s, separator="_", lowercase=True)


def check_filename(full_filename: str, extensions: set[str]) -> None:
split = full_filename.split(".")
if len(split) != 2:
if len(split) < 2:
raise ValueError("The full filename with extension should be provided")
raise ValueError("Files cannot contain '.' in their name")
name, extension = split
if extension.lower() not in extensions:
raise ValueError(
f"This file extension is not supported. Extensions supported: "
f"{humanize_list([*extensions])}"
)
25 changes: 18 additions & 7 deletions src/ouranos/web_server/routes/services/wiki.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
APIRouter, Body, Depends, HTTPException, Query, Path, status, UploadFile)
from sqlalchemy.ext.asyncio import AsyncSession

from ouranos.core.config.consts import MAX_PICTURE_FILE_SIZE, MAX_TEXT_FILE_SIZE
from ouranos.core.config.consts import (
MAX_PICTURE_FILE_SIZE, MAX_TEXT_FILE_SIZE, SUPPORTED_IMAGE_EXTENSIONS,
SUPPORTED_TEXT_EXTENSIONS)
from ouranos.core.database.models.app import (
ServiceName, UserMixin, WikiArticleNotFound, WikiArticle, WikiArticleModification,
WikiArticlePicture, WikiTag, WikiTopic)
from ouranos.core.utils import check_filename
from ouranos.web_server.auth import get_current_user, is_operator
from ouranos.web_server.dependencies import get_session
from ouranos.web_server.routes.services.utils import service_enabled
Expand Down Expand Up @@ -393,13 +396,15 @@ async def upload_article(
current_user: Annotated[UserMixin, Depends(get_current_user)],
session: Annotated[AsyncSession, Depends(get_session)],
):
if not file.filename.endswith(".md"):
try:
check_filename(file.filename, SUPPORTED_TEXT_EXTENSIONS)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="File should be a valid '.md' file"
detail=f"{e}"
)
filename = file.filename.rstrip(".md")
try:
await topic_or_abort(session, topic_name)
if file.size > MAX_TEXT_FILE_SIZE:
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
Expand All @@ -409,7 +414,7 @@ async def upload_article(
await WikiArticle.create(
session,
topic_name=topic_name,
name=filename,
name=file.filename.split(".")[0],
values={
"content": content.decode("utf-8"),
"author_id": current_user.id,
Expand Down Expand Up @@ -572,7 +577,13 @@ async def upload_picture(
file: UploadFile,
session: Annotated[AsyncSession, Depends(get_session)],
):
filename = file.filename.rstrip(".md")
try:
check_filename(file.filename, SUPPORTED_IMAGE_EXTENSIONS)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"{e}"
)
try:
await article_or_abort(session, topic_name, article_name)
if file.size > MAX_PICTURE_FILE_SIZE:
Expand All @@ -585,7 +596,7 @@ async def upload_picture(
session,
topic_name=topic_name,
article_name=article_name,
name=filename,
name=file.filename.split(".")[0],
values={
"content": content,
},
Expand Down
23 changes: 22 additions & 1 deletion tests/web_server/routes/wiki.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,12 +271,33 @@ async def test_create_picture_unauthorized(client: TestClient):
assert response.status_code == 403


@pytest.mark.asyncio
async def test_create_picture_invalid_name(
client_operator: TestClient,
db: AsyncSQLAlchemyWrapper,
):
name = "invalid.name"
content = b"def not a true picture"
payload = {
"name": name,
"extension": ".jpeg",
"content": content.decode("utf-8"),
}
# Run test
response = client_operator.post(
f"/api/app/services/wiki/topics/u/{wiki_topic_name}/"
f"u/{wiki_article_name}/u",
json=payload,
)
assert response.status_code == 422


@pytest.mark.asyncio
async def test_create_picture(
client_operator: TestClient,
db: AsyncSQLAlchemyWrapper,
):
name = "Cute.plant.picture"
name = "Cute plant picture"
content = b"def not a true picture"
payload = {
"name": name,
Expand Down

0 comments on commit dcacc8e

Please sign in to comment.