Skip to content

Remove pickle from forms and Celery serializer#14791

Merged
Maffooch merged 8 commits intodevfrom
security/remove-pickle
May 4, 2026
Merged

Remove pickle from forms and Celery serializer#14791
Maffooch merged 8 commits intodevfrom
security/remove-pickle

Conversation

@Maffooch
Copy link
Copy Markdown
Contributor

Summary

  • Replace pickle with json in the survey choice-question widget (MultiExampleField / MultiWidgetBasic) and dojo/survey/views.py — the payload is a list of up to 6 strings, JSON-native.
  • Make every Celery task argument JSON-serializable: async_delete_task takes (model_label, pk) and refetches; SLA recompute takes (sla_config_id, product_ids); per-channel notification sends run inline inside the surrounding async_create_notification task instead of dispatching an inner Celery task with model instances; user context is injected as async_user_id and resolved in the worker via Dojo_User.objects.filter(pk=...).first().
  • Flip DD_CELERY_TASK_SERIALIZER default to json, tighten CELERY_ACCEPT_CONTENT to ["json"] (drops pickle/msgpack/yaml), and pin CELERY_RESULT_SERIALIZER = "json". Drop the now-unneeded timedelta arg from the add-alerts beat schedule entry.
  • Add unittests/test_no_pickle.py (regression net: walks dojo/ for import pickle and asserts JSON-only Celery settings) and unittests/test_survey_forms.py (widget round-trip).

Why

pickle.loads() on attacker-controllable bytes is arbitrary code execution. Removing pickle from both sites eliminates the class of risk and aligns the Celery configuration with the JSON-only default that Celery has shipped for years.

Operator note

Workers running this version reject in-flight pickled messages from the previous version with ContentDisallowed. Drain the broker (`celery -A dojo purge -f`) or scale workers to zero before deploying. Beat-driven tasks (e.g. `add_alerts`) self-heal on the next tick.

Test plan

  • `grep -rin pickle dojo/` returns zero hits
  • `python manage.py check`
  • `python -m pytest unittests/test_no_pickle.py unittests/test_survey_forms.py -x`
  • `python -m pytest unittests/test_async_delete.py unittests/test_sla_calculations.py unittests/test_notifications.py -x`
  • Full unit-test suite via `run-unittest.sh`
  • Manual: create a Choice survey question, save it, verify choices persist
  • Manual: save an SLA_Configuration with new severities, verify findings recompute
  • Manual: delete a Product via UI, verify async cascade and audit trail records the requesting user
  • Manual: trigger a scan import, verify per-channel notifications fire (slack/mail/webhooks/msteams)

🤖 Generated with Claude Code

Pickle deserialization on attacker-controllable bytes is arbitrary code
execution. Two sites used pickle:

- The survey choice-question form pickled/unpickled a list of strings
  through MultiExampleField.compress / MultiWidgetBasic.decompress and
  pickle.loads in survey/views.py. Switched to json — the data is just a
  list of up to 6 strings.

- Celery defaulted to the pickle serializer with
  CELERY_ACCEPT_CONTENT including pickle/yaml/msgpack so dispatch sites
  could pass Django model instances and a Dojo_User on the wire. Made
  every task argument JSON-serializable: async_delete_task takes
  (model_label, pk) and refetches; SLA recompute takes
  (sla_config_id, product_ids); per-channel notification sends run
  inline inside the surrounding async_create_notification task instead
  of dispatching an inner Celery task with model instances; user
  context is injected as async_user_id and resolved in the worker.
  Flipped DD_CELERY_TASK_SERIALIZER default to json, tightened
  CELERY_ACCEPT_CONTENT to ["json"], and added
  CELERY_RESULT_SERIALIZER = "json".

Added unittests/test_no_pickle.py as a regression net (asserts no
pickle imports in dojo/ and that the Celery serializer settings stay
JSON-only) and unittests/test_survey_forms.py for the widget round-trip.

Operator note: workers running this version reject in-flight pickled
messages with ContentDisallowed. Drain the broker
(\`celery -A dojo purge -f\`) or scale workers to zero before deploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Maffooch Maffooch requested a review from mtesauro as a code owner April 30, 2026 02:55
@mtesauro mtesauro added this to the 2.58.0 milestone Apr 30, 2026
@github-actions github-actions Bot added settings_changes Needs changes to settings.py based on changes in settings.dist.py included in this PR unittests conflicts-detected labels Apr 30, 2026
@github-actions
Copy link
Copy Markdown
Contributor

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@Maffooch Maffooch changed the title security: remove pickle from forms and Celery serializer Remove pickle from forms and Celery serializer Apr 30, 2026
Resolved conflicts after dev's notifications module reorganization:
- dojo/tasks.py: accepted dev's re-export module (Celery tasks moved
  to dojo/notifications/tasks.py).
- dojo/notifications/helper.py: kept dev's import structure, retained
  the per-channel `.run()` synchronous calls so model instances no
  longer cross the wire as task arguments.
- dojo/notifications/tasks.py: applied the add_alerts signature
  change here (drop runinterval positional arg, inline timedelta).
- unittests/test_survey_forms.py: dropped the per-file pickle guard
  to satisfy ruff PLC0415; the broader guard in test_no_pickle.py
  already covers the entire dojo/ tree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Conflicts have been resolved. A maintainer will review the pull request shortly.

Maffooch and others added 3 commits April 29, 2026 22:09
The async_user_id user resolution and refetch in async_delete_task /
SLA recompute add 1-6 queries per scenario; CI auto-generated the
new expected counts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before the pickle removal, the SLA recompute task received the
SLA_Configuration / Product instances by reference (Celery's sync
.apply() does not serialize). The inner update function set
async_updating=False on those shared instances, so the dispatcher's
local self.async_updating ended up False as well.

After switching the dispatch to pass IDs and refetch in the task, the
inner function only resets async_updating on its refetched copies.
The dispatcher's in-memory self.async_updating stayed True, so a
subsequent save() on the same instance triggered the lock-revert path
at SLA_Configuration.save() line 1058 and overwrote the caller's
field changes (e.g. enforce_critical) with the DB values.

Manifested as test_sla_expiration_date_after_sla_not_enforced failing:
sla_config.enforce_critical=False was reverted to True on save.

Reset async_updating on the in-memory caller instances after
dispatch returns to keep them consistent with the post-task DB state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mtesauro mtesauro requested a review from valentijnscholten May 1, 2026 03:10
Copy link
Copy Markdown
Contributor

@mtesauro mtesauro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved

@Maffooch Maffooch requested review from Jino-T, blakeaowens and rossops May 3, 2026 15:36
@rossops
Copy link
Copy Markdown
Collaborator

rossops commented May 4, 2026

Wrapped each of the four per-channel .run() calls (Slack, MSTeams, Mail, Webhooks) in try/except Exception with logger.exception(...) for the failed channel. A failure in one channel will now log a traceback and let the remaining channels proceed, matching the prior behavior.

@paulOsinski paulOsinski self-requested a review May 4, 2026 14:53
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 4, 2026

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 4, 2026

Conflicts have been resolved. A maintainer will review the pull request shortly.

@Maffooch Maffooch merged commit c1981f3 into dev May 4, 2026
159 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

settings_changes Needs changes to settings.py based on changes in settings.dist.py included in this PR unittests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants