Private
Public Access
Play video attachments inline, with an expand option (#65)
A video file previously rendered as a generic downloadable file card, same as any other attachment. The file-serve endpoint forces Content-Disposition: attachment for every upload as an XSS mitigation (a same-origin-served .html/.svg executing script), which also meant a <video> tag pointed at it couldn't play -- the browser would just try to download it. Carve out a strict, server-side allowlist (video/mp4, video/webm, video/ogg -- deliberately not "every video/* type") that skips the forced download, the same reasoning MessageImage's own endpoint already relies on: these are content types a browser only ever interprets as media, never as something that could execute script. Anything else, including other video formats like .mov, still forces a download exactly as before. On the frontend, a video attachment with one of those content types renders as an inline <video controls> instead of the generic file card, with a hover-revealed expand button that calls the browser's native Fullscreen API on the video element directly rather than building a second lightbox component. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -99,6 +99,43 @@ async def test_serve_file_forces_download(client, db_session):
|
||||
assert "notes.txt" in disposition
|
||||
|
||||
|
||||
async def test_serve_allowlisted_video_inline(client, db_session):
|
||||
# #65: a <video> tag can't play something the browser is forced to
|
||||
# download instead -- browser-playable video types are the one carve-
|
||||
# out from test_serve_file_forces_download's rule above.
|
||||
await register_and_login(client, db_session, username=_unique("alice"))
|
||||
room = (await client.post("/api/rooms", json={"name": _unique("general")})).json()
|
||||
upload = await client.post(
|
||||
f"/api/rooms/{room['id']}/files",
|
||||
files={"file": ("clip.mp4", b"not a real mp4", "video/mp4")},
|
||||
)
|
||||
file_id = upload.json()["id"]
|
||||
|
||||
resp = await client.get(f"/api/rooms/{room['id']}/files/{file_id}")
|
||||
assert resp.status_code == 200
|
||||
assert "content-disposition" not in resp.headers
|
||||
assert resp.headers["content-type"] == "video/mp4"
|
||||
|
||||
|
||||
async def test_serve_non_allowlisted_video_still_forces_download(client, db_session):
|
||||
# video/quicktime (.mov) has spotty <video> support outside Safari, and
|
||||
# more importantly this proves the carve-out is a strict allowlist, not
|
||||
# "every video/* content type" -- the security-relevant boundary from
|
||||
# test_serve_file_forces_download must still hold for anything not on
|
||||
# INLINE_SAFE_VIDEO_CONTENT_TYPES.
|
||||
await register_and_login(client, db_session, username=_unique("alice"))
|
||||
room = (await client.post("/api/rooms", json={"name": _unique("general")})).json()
|
||||
upload = await client.post(
|
||||
f"/api/rooms/{room['id']}/files",
|
||||
files={"file": ("clip.mov", b"not a real mov", "video/quicktime")},
|
||||
)
|
||||
file_id = upload.json()["id"]
|
||||
|
||||
resp = await client.get(f"/api/rooms/{room['id']}/files/{file_id}")
|
||||
assert resp.status_code == 200
|
||||
assert "attachment" in resp.headers["content-disposition"]
|
||||
|
||||
|
||||
def _register_ws(ws_client, username: str) -> dict:
|
||||
from app.schemas.user import UserCreate
|
||||
from app.services.auth_service import register_user
|
||||
|
||||
Reference in New Issue
Block a user