Skip to main content

🛡️ Deployment

Everyone who can log in to the admin can change your content. This page lists what to configure before you go live, what Content Studio protects out of the box, and the system checks that catch configuration mistakes.

Deployment checklist​

  • Serve the admin over HTTPS. The session cookie is marked Secure whenever DEBUG is off (REFRESH_COOKIE_SECURE). Behind a proxy that terminates TLS, also set Django's SECURE_PROXY_SSL_HEADER.
  • Configure a shared cache, such as Redis, Memcached or the database cache. The login and password reset limits are counted in the cache; with the default per-process cache, every worker counts separately. Ended sessions are recorded in the cache (TOKEN_CACHE); with a per-process cache, a session logged out in one worker continues in another. Presence is kept in the cache too.
  • Pass on the client's IP address. The limits count per IP address. Without further configuration, Content Studio uses the address of the connection and ignores X-Forwarded-For, which any client can set. Behind a proxy, every request then comes from the proxy's address and all clients share one limit. Set NUM_PROXIES in the Django REST Framework settings to the number of proxies in front of your application, and make sure those proxies add the client's address to X-Forwarded-For.
  • Serve uploads from a separate domain when you can, such as a CDN or an object storage bucket. Even with the upload checks, a file served from your own domain runs with your site's privileges if a browser renders it.
  • Clear unsaved uploads daily. Files picked in a FileWidget but never saved stay in the upload folder until manage.py content_studio_clear_uploads removes them. Run it once a day, for example with cron.
  • Configure a task backend that runs Django tasks in a worker. Password reset emails are then sent outside the request, so its duration does not reveal whether an address has an account (password reset).
  • Keep DEBUG off in production, and keep Django, Content Studio and your other dependencies up to date.
  • Use Django's security middleware and settings: SecurityMiddleware with HSTS, XFrameOptionsMiddleware, and SESSION_COOKIE_SECURE and CSRF_COOKIE_SECURE for the rest of your site. manage.py check --deploy lists what is missing.
  • Keep SECRET_KEY strong and secret, or set a separate TOKEN_SIGNING_KEY: it signs the sessions.

What Content Studio protects​

  • Staff only. Like the classic Django admin, only active staff users can log in, and every request checks this again. Deactivating a user or revoking their staff status ends their access immediately. What an editor may see and do is decided by the permission hooks.
  • Short sessions. Sessions renew themselves while an editor works and end after a week without use, and 30 days after logging in at the latest (MAX_SESSION_LIFETIME). Changing or resetting a password logs the user out everywhere (authentication).
  • Logging out ends the session on the server. A copy of the session cookie stops working at logout, and each renewal invalidates the previous cookie. A cookie presented again after its renewal was copied, so Content Studio ends that session.
  • Limited login and password reset attempts, per IP address (LOGIN_THROTTLE_RATE, PASSWORD_RESET_THROTTLE_RATE). The address is the one the connection comes from; an X-Forwarded-For header sent by the client is ignored unless you configure your proxies (see the checklist below). Five wrong reset codes lock an email address out, and new passwords must pass AUTH_PASSWORD_VALIDATORS.
  • Sanitized rich text. HTMLField cleans its value on save with an allowlist sanitizer (HTML_SANITIZER).
  • Checked uploads. The media library accepts a list of file types up to a maximum size, and rejects markup such as HTML and SVG (MEDIA_LIBRARY_ALLOWED_EXTENSIONS). File fields reject markup too, and a file is only attached to an object by the editor who uploaded it.
  • Write-only secrets. A field with PasswordWidget(write_only=True) never sends its value to the browser or to an export.
  • Nothing public but the login pages. Before logging in, the admin only receives what it needs to show the login form.

System checks​

Content Studio checks its configuration with Django's system check framework: on manage.py check, when the development server starts, on makemigrations and in your CI. An error means the configuration is broken and would fail at runtime, often silently. A warning means it works, but probably not as you intended. Neither stops the server from starting, but manage.py check exits with an error code.

Errors​

IDReports
content_studio.E001list_display set to a mapping. The list view's mapping belongs in list_item_display; list_display sets the table columns.
content_studio.E002list_views with an unknown view name.
content_studio.E003Two extensions with the same ID. Pass an extension_id to tell them apart.
content_studio.E004Two dashboard widgets with the same ID, on the start page and the dashboard pages together. Pass a widget_id to tell them apart.
content_studio.E005Two components with the same ID on one studio, or on an inline and the inlined model's own studio. Pass a component_id to tell them apart; the same component class may serve several studios.
content_studio.E006table_display entries that are not field names or TableColumn objects, duplicate columns, columns that are not a model field, a model property, __str__ or a studio method, and formats that are not content_studio.formats classes or instances. Inlines are checked too.
content_studio.E007Bulk actions: an actions list with unknown or duplicate names, a @bulk_action that replaces the built-in delete or export, "export" in actions without export, or an unknown permission.
content_studio.E008soft_delete naming an unknown field, or a field that is not a nullable date or date and time.
content_studio.E009A FOCAL_POINT_ATTR setting, which is removed: the focal point is one of a media item's properties.
content_studio.E010Edit page fields that are not model fields or properties, which would show as empty read-only boxes, and Field(widget=...) values that are not content_studio.widgets classes or instances.
content_studio.E011search_fields, ordering and list_filter entries that are not a field, and list_item_display values that are not a field or a model method. Inlines are checked too.
content_studio.E012An inline's fk_name that is not a field, or not a foreign key (or one-to-one field) to the parent model; or no fk_name on an inline whose model has no, or more than one, foreign key to the parent. The inline is not served.
content_studio.E013Values in the site's default_widget_mapping or default_format_mapping that are not widget or format classes or instances.
content_studio.E014A MEDIA_LIBRARY_ALLOWED_EXTENSIONS that is not a list of extensions without the dot, or a MEDIA_LIBRARY_MAX_UPLOAD_SIZE that is not a positive number of bytes.
content_studio.E015readonly_fields entries that are not model fields or properties. Inlines are checked too.
content_studio.E016prepopulated_fields that is not a mapping, fields or source fields that are not model fields, fields that cannot be prepopulated (DateTimeField, ForeignKey, OneToOneField, ManyToManyField) and fields in readonly_fields. Inlines are checked too.
content_studio.E017A reorder_field that is not a model field or, unless the studio overrides reorder, not a number field, and reorder_scope entries that are not model fields. Inlines are checked too.
content_studio.E018A BlockField without blocks, with entries in blocks that are not Block subclasses, with two blocks of the same name, with a negative min_num or max_num, or with min_num greater than max_num. Also block fields that a block cannot store: ManyToManyField, ManyMediaField, FileField and auto fields, a BlockField in a block inside a block (blocks inside blocks go one level deep), and relations to models that are not installed.
content_studio.E019Webhook settings: a WEBHOOK_ALLOWED_HOSTS that is not a list of host names, a WEBHOOK_MAX_ATTEMPTS that is not a positive whole number, a WEBHOOK_TIMEOUT that is not a positive number of seconds, or a WEBHOOK_DELIVERY_RETENTION that is not None or a positive whole number of days.
content_studio.E020An edit_header that is not a list, or entries in it that are not components, such as fields, or are text components (Title, Subtitle, Text, Callout) or an SEOPreview. Inlines are checked too.
content_studio.E021A duplicate_inlines that is not a list, or entries in it that are not in the studio's inlines.
content_studio.E022An export_fields that is not a list, or entries in it that are not field names or TableColumn objects, that appear twice, or that do not resolve to a model field, a model property, __str__ or a method on the studio.
content_studio.E023A PRESENCE_CACHE that is not a cache in CACHES.
content_studio.E024An edit_width that is not "default" or "wide". Inlines are checked too.
content_studio.E025A MEDIA_METADATA_ATTR you set without a MEDIA_LIBRARY_MODEL, or naming a field that is missing or not a JSONField. The default is not checked.
content_studio.E026A MEDIA_PROPERTIES_ATTR you set without a MEDIA_LIBRARY_MODEL, or naming a field that is missing or not a JSONField. The default is not checked.
content_studio.E027A THUMBNAIL_CACHE that is not a cache in CACHES, while thumbnails are on.
content_studio.E028A DashboardPage path that one of the admin's own pages uses, such as content/ or media-library/.
content_studio.E029A dashboard widget's refresh_interval that is not a positive number of seconds or None.
content_studio.E030A NOTIFICATION_RETENTION or NOTIFICATION_UNREAD_RETENTION that is not a positive whole number of days or None.
content_studio.E031A notification event registered with register_event whose key is not lowercase dotted words (like blog.article_submitted), starts with content., has an unknown level, or is registered twice with different definitions.
content_studio.E032A NOTIFICATION_EMAIL_DIGEST that is not True or False, an ADMIN_URL that is not an absolute http or https URL, or the digest turned on without ADMIN_URL.
content_studio.E033An IMAGE_SIZES size with an invalid name or option, such as an unknown option, no width or height, or "cover" without both. The size is not made.
content_studio.E034A MAP_TILE_URL without {z}, {x} and {y}, a MAP_TILE_ATTRIBUTION that is not a string, a MAP_GEOCODER_URL that is not None or an http(s) URL, or an OEMBED_PROVIDERS that is not None or a list of providers with a name, an http(s) endpoint and valid URL patterns.
content_studio.E035A MapWidget on a field that is not a JSONField, or a FloatField or DecimalField with an lng_field naming another number field; or lng_field on a JSONField.
content_studio.E036An SEOPreview whose title_field, description_field or path_field is not a model field.
content_studio.E037Analytics set up wrong: an AnalyticsPage, link, Analytics component or analytics widget without an AnalyticsExtension, more than one extension or page, a provider that reports metrics or breakdowns Content Studio does not know, or an analytics page path that another page uses.
content_studio.E038The same model inlined twice on one studio. An inline is served at one endpoint per parent and model, so the second inline is not served.
content_studio.E039With TENANT_MODEL set, a registered or inlined model with more than one foreign key to the tenant model. Tenant scoping needs exactly one.
content_studio.E040A condition (when) that reads a field the model does not have, or an excluded or write-only field, whose value never reaches the admin.

Warnings​

IDReports
content_studio.W001A required model field missing from edit_main and edit_sidebar, so saving fails. Fields with a default, nullable or blank fields, and fields Content Studio fills in itself do not count.
content_studio.W002A required model field set to readonly=True on the edit page or listed in readonly_fields, so it can never be filled in and saving fails.
content_studio.W003A choice widget (SelectWidget, MultiSelectWidget, ToggleGroupWidget, RadioButtonWidget) on a field without choices, which would show empty.
content_studio.W004sortable_by names that are not table_display columns. Sorting only applies to table columns.
content_studio.W005list_filter entries that are not field names, such as filter classes or tuples. They are ignored.
content_studio.W006A model group with a model that is not registered with the site, which leaves an empty menu entry.
content_studio.W007A media item model without file, name or type, or a folder model without name or parent.
content_studio.W008A crop_image method on the site without a MEDIA_LIBRARY_MODEL, so it is never called.
content_studio.W009An empty LOGIN_BACKENDS setting, so nobody can log in.
content_studio.W010list_display used for the table columns. It works, but rename it to table_display. When both are set, list_display is ignored. Inlines are checked too.
content_studio.W011A studio that overrides get_list_display, which is never called. The list view uses get_list_item_display.
content_studio.W012A dashboard widget that does not subclass a built-in widget, which the admin cannot show.
content_studio.W013An ordering that does not start with the reorder_field (after any reorder_scope fields), so moves do not match the order of the list. Inlines are checked too.
content_studio.W014allow_duplication on a singleton, or duplicate_inlines without allow_duplication. Neither has an effect.
content_studio.W015export_fields or allow_export on a singleton, an empty export_fields, or exported columns that are in exclude and are left out. None of them has an effect.
content_studio.W016Presence kept in a DummyCache, which keeps nothing, so the admin never shows who has an object open.
content_studio.W017Presence kept in a LocMemCache, which each process keeps for itself. A deployment check: manage.py check --deploy reports it.
content_studio.W018Password reset emails sent during the request, because no task backend runs Django tasks in a worker. A deployment check: manage.py check --deploy reports it.
content_studio.W019CoverImageWidget on a field that is not a MediaField to the media library model. The widget edits one media item.
content_studio.W020A Huey task dashboard without Huey's stats app, so it cannot show runs, durations or errors.
content_studio.W021A NotificationsWidget on a site without content_studio.notifications in INSTALLED_APPS; it is not shown.
content_studio.W022IMAGE_SIZES set while no sizes can be made: Pillow is not installed, IMAGE_SIZE_FOLDER is not set, or the media model has no metadata field. Every size is then the image's own file.
content_studio.W023An analytics provider missing a setting it needs, such as an UmamiProvider without an api_key or website_id. Its reports show the statistics as unavailable.
content_studio.W024A required model field that only shows under a condition. A hidden field is not saved, so creating an object while it is hidden fails.

Reporting a vulnerability​

Report security problems privately, through GitHub's private vulnerability reporting or by email; see SECURITY.md.