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.
This commit is contained in:
+2
-2
@@ -2,10 +2,10 @@
|
||||
branch = true
|
||||
|
||||
source =
|
||||
cvat/apps/
|
||||
cvat.apps
|
||||
utils.dataset_manifest
|
||||
cvat-sdk/
|
||||
cvat-cli/
|
||||
utils/dataset_manifest
|
||||
|
||||
omit =
|
||||
cvat/settings/*
|
||||
|
||||
@@ -221,7 +221,7 @@ jobs:
|
||||
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml \
|
||||
run --env PYTHONDEVMODE=1 cvat_ci /bin/bash \
|
||||
-c 'python manage.py test cvat/apps -v 2'
|
||||
-c 'python manage.py test cvat.apps -v 2'
|
||||
|
||||
- name: Creating a log file from cvat containers
|
||||
if: failure()
|
||||
|
||||
@@ -288,7 +288,7 @@ jobs:
|
||||
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml \
|
||||
run --env PYTHONDEVMODE=1 cvat_ci /bin/bash \
|
||||
-c 'coverage run -a manage.py test -v 2 cvat/apps && coverage json && mv coverage.json ${CONTAINER_COVERAGE_DATA_DIR}/unit_tests_coverage.json'
|
||||
-c 'coverage run -a manage.py test -v 2 cvat.apps && coverage json && mv coverage.json ${CONTAINER_COVERAGE_DATA_DIR}/unit_tests_coverage.json'
|
||||
|
||||
- name: Uploading code coverage results as an artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
|
||||
@@ -170,7 +170,7 @@ jobs:
|
||||
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml \
|
||||
run --env PYTHONDEVMODE=1 cvat_ci /bin/bash \
|
||||
-c 'python manage.py test cvat/apps -v 2'
|
||||
-c 'python manage.py test cvat.apps -v 2'
|
||||
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml down -v
|
||||
|
||||
|
||||
+20
-9
@@ -1,3 +1,5 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG BASE_IMAGE=ubuntu:24.04
|
||||
|
||||
FROM ${BASE_IMAGE} AS build-image-base
|
||||
@@ -144,9 +146,9 @@ COPY --from=build-smokescreen /tmp/smokescreen /usr/local/bin/smokescreen
|
||||
|
||||
# Add a non-root user
|
||||
ENV USER=${USER}
|
||||
ENV HOME /home/${USER}
|
||||
RUN deluser --remove-home ubuntu && \
|
||||
adduser --uid=1000 --shell /bin/bash --disabled-password --gecos "" ${USER}
|
||||
ENV CVAT_BASE_DIR=/home/${USER}
|
||||
|
||||
ARG CLAM_AV="no"
|
||||
RUN if [ "$CLAM_AV" = "yes" ]; then \
|
||||
@@ -186,12 +188,21 @@ RUN python -m pip uninstall -y pip
|
||||
|
||||
# Install and initialize CVAT, copy all necessary files
|
||||
COPY cvat/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY --chown=${USER} supervisord/ ${HOME}/supervisord
|
||||
COPY --chown=${USER} backend_entrypoint.d/ ${HOME}/backend_entrypoint.d
|
||||
COPY --chown=${USER} manage.py rqscheduler.py backend_entrypoint.sh wait_for_deps.sh ${HOME}/
|
||||
COPY --chown=${USER} utils/ ${HOME}/utils
|
||||
COPY --chown=${USER} cvat/ ${HOME}/cvat
|
||||
COPY --chown=${USER} components/analytics/clickhouse/init.py ${HOME}/components/analytics/clickhouse/init.py
|
||||
COPY --parents \
|
||||
backend_entrypoint.d cvat supervisord utils \
|
||||
backend_entrypoint.sh \
|
||||
components/analytics/clickhouse/init.py \
|
||||
manage.py \
|
||||
rqscheduler.py \
|
||||
wait_for_deps.sh \
|
||||
/opt/cvat/
|
||||
|
||||
RUN python -m compileall -q /opt/cvat
|
||||
|
||||
# Link manage.py to the home directory for backwards compatibility.
|
||||
RUN ln -s /opt/cvat/manage.py ${CVAT_BASE_DIR}/manage.py
|
||||
|
||||
RUN echo "/opt/cvat" > /opt/venv/lib/python3.12/site-packages/cvat.pth
|
||||
|
||||
ARG COVERAGE_PROCESS_START
|
||||
RUN if [ "${COVERAGE_PROCESS_START}" ]; then \
|
||||
@@ -201,9 +212,9 @@ RUN if [ "${COVERAGE_PROCESS_START}" ]; then \
|
||||
# RUN all commands below as 'django' user.
|
||||
# Use numeric UID/GID so that the image is compatible with the Kubernetes runAsNonRoot setting.
|
||||
USER 1000:1000
|
||||
WORKDIR ${HOME}
|
||||
WORKDIR ${CVAT_BASE_DIR}
|
||||
|
||||
RUN mkdir -p data share keys logs /tmp/supervisord /tmp/cvat static
|
||||
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["./backend_entrypoint.sh"]
|
||||
ENTRYPOINT ["/opt/cvat/backend_entrypoint.sh"]
|
||||
|
||||
+13
-11
@@ -2,6 +2,8 @@
|
||||
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
|
||||
fail() {
|
||||
printf >&2 "%s: %s\n" "$0" "$1"
|
||||
exit 1
|
||||
@@ -37,21 +39,21 @@ cmd_bash() {
|
||||
|
||||
cmd_init() {
|
||||
wait_for_db
|
||||
~/manage.py migrate
|
||||
django-admin migrate
|
||||
|
||||
wait_for_redis_inmem
|
||||
~/manage.py migrateredis
|
||||
~/manage.py syncperiodicjobs
|
||||
django-admin migrateredis
|
||||
django-admin syncperiodicjobs
|
||||
|
||||
if [[ "${CVAT_ANALYTICS:-0}" == "1" ]]; then
|
||||
wait_for_clickhouse
|
||||
python components/analytics/clickhouse/init.py
|
||||
python "$SCRIPT_DIR/components/analytics/clickhouse/init.py"
|
||||
fi
|
||||
}
|
||||
|
||||
_load_component_config() {
|
||||
declare -gA merged_config=()
|
||||
for config_file in ~/backend_entrypoint.d/*.conf; do
|
||||
for config_file in "$SCRIPT_DIR/backend_entrypoint.d/"*.conf; do
|
||||
declare -A config=$(cat $config_file)
|
||||
for key in "${!config[@]}"; do
|
||||
if [[ -n ${merged_config[$key]+_} ]]; then
|
||||
@@ -86,7 +88,7 @@ _get_includes() {
|
||||
_get_reusable_includes() {
|
||||
extra_configs=()
|
||||
for include in "$@"; do
|
||||
if ! [ -r "$HOME/supervisord/reusable/$include.conf" ]; then
|
||||
if ! [ -r "$SCRIPT_DIR/supervisord/reusable/$include.conf" ]; then
|
||||
fail "Unexpected supervisor include: $include"
|
||||
fi
|
||||
|
||||
@@ -114,24 +116,24 @@ cmd_run() {
|
||||
esac
|
||||
|
||||
if [ "$component" = "nginx" ]; then
|
||||
exec supervisord -c "supervisord/nginx.conf"
|
||||
exec supervisord -c "$SCRIPT_DIR/supervisord/nginx.conf"
|
||||
fi
|
||||
|
||||
if [ "$component" = "server" ]; then
|
||||
account_for_internal_proxy
|
||||
~/manage.py collectstatic --no-input
|
||||
django-admin collectstatic --no-input
|
||||
fi
|
||||
|
||||
wait_for_db
|
||||
|
||||
echo "waiting for migrations to complete..."
|
||||
while ! ~/manage.py migrate --check; do
|
||||
while ! django-admin migrate --check; do
|
||||
sleep 10
|
||||
done
|
||||
|
||||
wait_for_redis_inmem
|
||||
echo "waiting for Redis migrations to complete..."
|
||||
while ! ~/manage.py migrateredis --check; do
|
||||
while ! django-admin migrateredis --check; do
|
||||
sleep 10
|
||||
done
|
||||
|
||||
@@ -175,7 +177,7 @@ cmd_run() {
|
||||
export CVAT_POSTGRES_APPLICATION_NAME=$postgres_app_name
|
||||
export CVAT_SUPERVISORD_INCLUDES=$supervisord_includes
|
||||
|
||||
exec supervisord -c "supervisord/$component.conf"
|
||||
exec supervisord -c "$SCRIPT_DIR/supervisord/$component.conf"
|
||||
}
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
### Changed
|
||||
|
||||
- In the backend server image, CVAT source files are now installed in
|
||||
`/opt/cvat` rather than `/home/django`, and owned by root
|
||||
(<https://github.com/cvat-ai/cvat/pull/10575>)
|
||||
@@ -32,7 +32,7 @@ from cvat import __version__
|
||||
from cvat.apps.iam.password_validation import DEFAULT_MIN_PASSWORD_LENGTH
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / ...
|
||||
BASE_DIR = Path(__file__).parents[2]
|
||||
BASE_DIR = Path(os.environ.get("CVAT_BASE_DIR", Path(__file__).parents[2]))
|
||||
|
||||
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")
|
||||
INTERNAL_IPS = ["127.0.0.1"]
|
||||
|
||||
@@ -218,8 +218,7 @@ The name of the service account to use for backend pods
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- python
|
||||
- manage.py
|
||||
- django-admin
|
||||
- workerprobe
|
||||
{{- range .args }}
|
||||
- {{ . | quote }}
|
||||
|
||||
@@ -29,7 +29,7 @@ services:
|
||||
environment:
|
||||
DJANGO_SETTINGS_MODULE: settings
|
||||
volumes:
|
||||
- ./settings.py:/home/django/settings.py:ro
|
||||
- ./settings.py:/opt/cvat/settings.py:ro
|
||||
```
|
||||
|
||||
### Active Directory Example
|
||||
|
||||
@@ -82,7 +82,7 @@ docker run -it --rm -u "$(id -u)":"$(id -g)" \
|
||||
-v "${PWD}":"/local" \
|
||||
--entrypoint python3 \
|
||||
cvat/server \
|
||||
utils/dataset_manifest/create.py --output-dir /local /local/<path/to/sources>
|
||||
/opt/cvat/utils/dataset_manifest/create.py --output-dir /local /local/<path/to/sources>
|
||||
```
|
||||
|
||||
Make sure to adapt the command to your file locations.
|
||||
@@ -151,10 +151,10 @@ Create a dataset manifest using Docker image:
|
||||
|
||||
```bash
|
||||
docker run -it --rm -u "$(id -u)":"$(id -g)" \
|
||||
-v ~/Documents/data/:${HOME}/manifest/:rw \
|
||||
--entrypoint '/usr/bin/bash' \
|
||||
-v ~/Documents/data/:/mnt/manifest/:rw \
|
||||
--entrypoint python3 \
|
||||
cvat/server \
|
||||
utils/dataset_manifest/create.py --output-dir ~/manifest/ ~/manifest/images/
|
||||
/opt/cvat/utils/dataset_manifest/create.py --output-dir /mnt/manifest/ /mnt/manifest/images/
|
||||
```
|
||||
|
||||
### File format
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
[program:rqscheduler]
|
||||
command=%(ENV_HOME)s/wait_for_deps.sh
|
||||
python3 %(ENV_HOME)s/rqscheduler.py
|
||||
command=/opt/cvat/wait_for_deps.sh
|
||||
python3 /opt/cvat/rqscheduler.py
|
||||
--host "%(ENV_CVAT_REDIS_INMEM_HOST)s"
|
||||
--port "%(ENV_CVAT_REDIS_INMEM_PORT)s"
|
||||
--password "%(ENV_CVAT_REDIS_INMEM_PASSWORD)s"
|
||||
-i 30
|
||||
--path %(ENV_HOME)s
|
||||
environment=VECTOR_EVENT_HANDLER="SynchronousLogstashHandler"
|
||||
numprocs=1
|
||||
autorestart=true
|
||||
|
||||
@@ -3,8 +3,8 @@ files = reusable/supervisord.conf %(ENV_CVAT_SUPERVISORD_INCLUDES)s
|
||||
|
||||
[fcgi-program:uvicorn]
|
||||
socket=unix:///tmp/cvat/uvicorn.sock
|
||||
command=%(ENV_HOME)s/wait_for_deps.sh
|
||||
python3 -m uvicorn
|
||||
command=/opt/cvat/wait_for_deps.sh
|
||||
uvicorn
|
||||
--fd 0
|
||||
--forwarded-allow-ips='*'
|
||||
cvat.asgi:application
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
files = reusable/supervisord.conf %(ENV_CVAT_SUPERVISORD_INCLUDES)s
|
||||
|
||||
[program:rqworker-pool]
|
||||
command=%(ENV_HOME)s/wait_for_deps.sh
|
||||
python3 %(ENV_HOME)s/manage.py rqworker-pool -v 3 %(ENV_CVAT_QUEUES)s
|
||||
command=/opt/cvat/wait_for_deps.sh
|
||||
django-admin rqworker-pool -v 3 %(ENV_CVAT_QUEUES)s
|
||||
--worker-class cvat.rqworker.DefaultWorker --num-workers %(ENV_NUMWORKERS)s %(ENV_CVAT_RQWORKER_EXTRA_FLAGS)s
|
||||
environment=VECTOR_EVENT_HANDLER="SynchronousLogstashHandler"
|
||||
numprocs=%(ENV_NUMPROCS)s
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
files = reusable/supervisord.conf %(ENV_CVAT_SUPERVISORD_INCLUDES)s
|
||||
|
||||
[program:rqworker]
|
||||
command=%(ENV_HOME)s/wait_for_deps.sh
|
||||
python3 %(ENV_HOME)s/manage.py rqworker -v 3 %(ENV_CVAT_QUEUES)s
|
||||
command=/opt/cvat/wait_for_deps.sh
|
||||
django-admin rqworker -v 3 %(ENV_CVAT_QUEUES)s
|
||||
--worker-class cvat.rqworker.DefaultWorker %(ENV_CVAT_RQWORKER_EXTRA_FLAGS)s
|
||||
environment=VECTOR_EVENT_HANDLER="SynchronousLogstashHandler"
|
||||
numprocs=%(ENV_NUMPROCS)s
|
||||
|
||||
@@ -5,10 +5,10 @@ sigterm = true
|
||||
concurrency=thread
|
||||
|
||||
source =
|
||||
cvat/apps/
|
||||
cvat.apps
|
||||
utils.dataset_manifest
|
||||
cvat-sdk/
|
||||
cvat-cli/
|
||||
utils/dataset_manifest
|
||||
|
||||
omit =
|
||||
cvat/settings/*
|
||||
|
||||
@@ -21,7 +21,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
CVAT_ROOT_DIR = next(dir.parent for dir in Path(__file__).parents if dir.name == "tests")
|
||||
CVAT_DB_DIR = ASSETS_DIR / "cvat_db"
|
||||
CLICKHOUSE_INIT_SCRIPT = "components/analytics/clickhouse/init.py"
|
||||
CLICKHOUSE_INIT_SCRIPT = "/opt/cvat/components/analytics/clickhouse/init.py"
|
||||
PREFIX = "test"
|
||||
|
||||
CONTAINER_NAME_FILES = ["docker-compose.tests.yml"]
|
||||
@@ -579,7 +579,7 @@ def session_finish(session):
|
||||
|
||||
def collect_code_coverage_from_containers():
|
||||
for container in Container.covered():
|
||||
process_command = "python3"
|
||||
process_command = "python"
|
||||
|
||||
# find process with code coverage
|
||||
pid = docker_exec(container, f"pidof {process_command} -o 1")
|
||||
|
||||
@@ -113,7 +113,7 @@ def generate_manifest(path: str) -> None:
|
||||
"--entrypoint",
|
||||
"python3",
|
||||
get_server_image_tag(),
|
||||
"utils/dataset_manifest/create.py",
|
||||
"/opt/cvat/utils/dataset_manifest/create.py",
|
||||
"--output-dir",
|
||||
"/local",
|
||||
"/local",
|
||||
|
||||
Reference in New Issue
Block a user