Please do not open a public issue for security problems.
Report privately through GitHub's Security Advisories feature: go to the Security tab of this repository and choose Report a vulnerability. That opens a private channel visible only to the maintainers.
Please include:
- what the issue is and roughly how severe you think it is,
- the steps or a proof of concept needed to reproduce it,
- the version or commit you tested,
- any deployment details that matter (Docker, reverse proxy, database).
You can expect an acknowledgement within about a week. We will keep you updated as we work on a fix and will credit you in the advisory unless you would rather stay anonymous. Please give us a reasonable chance to release a fix before disclosing publicly.
This project has no long-term support branches. Fixes land on main; please
test against main before reporting.
The defaults in this repository are chosen so a misconfigured deployment fails loudly rather than running insecurely. Work through this list before exposing an instance to the internet.
-
DJANGO_SECRET_KEYis set to a fresh random value. The app refuses to start without it whenDJANGO_DEBUGis off. Generate one with:bash python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())" -
MISTRAL_API_KEYis set and scoped to this application. Startup fails without it in production. -
RECAPTCHA_SECRET_KEYis set if the contact form is reachable. Startup only warns without it, and the form then rejects every submission — so an unset key is easy to miss. If you do not want the form, remove it rather than leaving it broken. - No
.envfile is committed..gitignoreexcludes them; verify withgit status --ignored. - Secrets are supplied via your platform's secret store, not baked into an
image. Note that Docker build args (
VITE_*) are visible in image history — the reCAPTCHA site key is public by design, but never pass a private key as a build arg.
-
DJANGO_DEBUGis unset or false. With debug on, tracebacks expose settings, environment values and source paths. -
DJANGO_ALLOWED_HOSTSlists explicit hostnames.*is rejected in production; a permissive value enables Host-header attacks. -
DJANGO_CORS_ALLOWED_ORIGINSlists only your frontend origin. It is empty by default, which blocks all cross-origin browser access. -
DJANGO_CSRF_TRUSTED_ORIGINSincludes your frontend origin if it differs from the API origin. -
DJANGO_CORS_ALLOW_CREDENTIALSstays off unless you genuinely need cookies on cross-origin requests. Never combine it with a wide origin list. - Run
python manage.py check --deploywith your production environment loaded. It must report no issues.
- TLS terminates in front of the app. Keep
DJANGO_SECURE_SSL_REDIRECTon unless the proxy already redirects. - Behind a reverse proxy, set
DJANGO_USE_X_FORWARDED_PROTO=Trueonly if the proxy strips client-suppliedX-Forwarded-Protoheaders. Trusting that header from an unfiltered source lets clients fake HTTPS. -
DJANGO_SECURE_HSTS_SECONDSis left at its one-year default once HTTPS is confirmed working. Set it to0while testing — HSTS is hard to undo.
- The Django admin (
/admin/) is restricted, or removed fromINSTALLED_APPSif unused. It exposes stored contact-form submissions. - The database file or server is not publicly reachable. With the default SQLite setup, keep the data volume off any web-served path.
- Backups of the database are encrypted — it holds personal data (see below).
-
MEDIA_ROOTis not served by a web server with directory listing enabled.
-
MAX_UPLOAD_SIZEandMAX_ARCHIVE_EXTRACTED_SIZEsuit your hosting. The defaults (200 MB and 500 MB) are permissive for a public instance. - A rate limit sits in front of
/ocr/upload/. The application does not rate-limit, and every upload costs a paid third-party OCR call — an unauthenticated public instance can be run up a bill by anyone. - Consider putting authentication in front of the API. There is none built in; all endpoints are open.
Operators of this software should know what it collects, because in many jurisdictions running a public instance makes you a data controller.
The contact form (POST /ocr/submit_form/) writes a row containing the
first name, last name, company, email address, selected products, and free-text
message supplied by the submitter, plus a timestamp. This data:
- is retained indefinitely — there is no automatic deletion or retention policy, and no built-in export or erasure tooling;
- is readable by anyone with Django admin access;
- is not encrypted at rest beyond whatever your database and disk provide.
If you operate a public instance, you are responsible for publishing a privacy notice, honouring access and deletion requests, and setting a retention period. Rows can be removed through the Django admin or the ORM.
- Uploaded documents and images are transmitted to the Mistral API for text extraction. Whatever a user uploads — which may include confidential or personal information — leaves your infrastructure. Review Mistral's data handling terms before processing sensitive material, and tell your users this happens.
- reCAPTCHA tokens are sent to Google for contact-form verification, and Google's widget observes visitors on pages where it is embedded.
The application does not log IP addresses itself, set analytics or tracking cookies, or store uploaded files after processing — uploads live in a temporary directory that is deleted once extraction completes. Note that your web server, reverse proxy, or hosting platform will very likely log client IP addresses independently of this application.
These are understood gaps, not oversights:
- No authentication or authorisation. Every endpoint is public.
- No rate limiting. See the note above about third-party API cost.
- No virus scanning of uploaded files. Uploads are parsed by LibreOffice during document conversion, which is a large attack surface; consider sandboxing the container if you accept untrusted input.
- Extracted text is rendered as HTML in the frontend. It is HTML-escaped before formatting is applied, but it originates in user-supplied documents.