Files
Roman Donchenko 4aa0be3df5 Prevent source files from being modified by the Django user (#10575)
Currently, all source files are located in `/home/django`, and owned by
`django`. This means that if there's any vulnerability that lets an
attacker overwrite files in the server, they can replace source files
with their own code, and potentially get that code executed. That's
pretty bad, so I want to harden against that.

Make all source files owned by root, and move them to `/opt/cvat`. Add a
`manage.py` symlink in `/home/django` for backwards compatibility. It
happens that if a script is a symlink, Python does not add the symlink's
directory to `sys.path`, which is great for us, since that lets us avoid
a writable directory on there.

Still, even though `/home/django/manage.py` is owned by root, an
attacker could potentially be able to delete it and replace it with
their own malicious file. To be a bit more safe, replace `~/manage.py`
calls in backend scripts with `django-admin`.

To make sure CVAT can still find the data directory, add a new
environment variable, `CVAT_BASE_DIR` and set it in the Docker image.

This also fixes a minor bug: we no longer override the `HOME`
environment variable in the `Dockerfile`, so now it's automatically set
by `Docker` depending on the current user.
2026-07-23 13:22:24 +03:00

132 lines
3.3 KiB
Python

# Copyright (C) CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
import io
import subprocess
from collections.abc import Generator
from fractions import Fraction
from io import BytesIO
from pathlib import Path
import av
import av.video.reformatter
import librosa
import numpy as np
from PIL import Image
from shared.fixtures.init import get_server_image_tag
def generate_image_file(filename="image.png", size=(100, 50), color=(0, 0, 0)):
f = BytesIO()
f.name = filename
image = Image.new("RGB", size=size, color=color)
image.save(f)
f.seek(0)
return f
def generate_image_files(
count: int,
*,
prefixes: list[str] | None = None,
filenames: list[str] | None = None,
sizes: list[tuple[int, int]] | None = None,
) -> list[BytesIO]:
assert not (prefixes and filenames), "prefixes cannot be used together with filenames"
assert not prefixes or len(prefixes) == count
assert not filenames or len(filenames) == count
images = []
for i in range(count):
prefix = prefixes[i] if prefixes else ""
filename = f"{prefix}{i}.jpeg" if not filenames else filenames[i]
image = generate_image_file(
filename, color=(i, i, i), **({"size": sizes[i]}) if sizes else {}
)
images.append(image)
return images
def generate_video_file(
num_frames: int,
*,
size: tuple[int, int] = (100, 50),
invalid_keyframes: bool = False,
) -> BytesIO:
f = BytesIO()
f.name = "video.mkv"
chapters = [
{
"id": 0,
"start": 0,
"end": 100,
"time_base": Fraction(1, 1000),
"metadata": {"title": "Intro"},
}
]
with av.open(f, "w") as container:
container.set_chapters(chapters)
stream = container.add_stream("mjpeg", rate=60)
stream.width = size[0]
stream.height = size[1]
stream.color_range = av.video.reformatter.ColorRange.JPEG
for i in range(num_frames):
frame = av.VideoFrame.from_image(Image.new("RGB", size=size, color=(i, i, i)))
for packet in stream.encode(frame):
if invalid_keyframes:
# Specify pts/dts values that result in 0 valid keyframes
packet.pts = 10
packet.dts = 10
container.mux(packet)
f.seek(0)
return f
def read_video_file(file: BytesIO) -> Generator[Image.Image, None, None]:
file.seek(0)
with av.open(file) as container:
video_stream = container.streams.video[0]
for frame in container.decode(video_stream):
yield frame.to_image()
def generate_manifest(path: str) -> None:
command = [
"docker",
"run",
"--rm",
"-u",
"root:root",
"-v",
f"{path}:/local",
"--entrypoint",
"python3",
get_server_image_tag(),
"/opt/cvat/utils/dataset_manifest/create.py",
"--output-dir",
"/local",
"/local",
]
try:
subprocess.check_output(command, stderr=subprocess.PIPE)
except subprocess.CalledProcessError as e:
print(e.stderr.decode("utf-8"))
raise
def read_audio_pcm(
f: Path | io.IOBase, *, offset_ms: int = 0, rate: int = 8000
) -> tuple[np.ndarray, float]:
return librosa.load(f, mono=True, sr=rate, offset=offset_ms / 1000)