Skip to content

Conversation

@WaVEV
Copy link
Collaborator

@WaVEV WaVEV commented Nov 12, 2025

INTPYTHON-827

Summary

This PR fixes unsafe handling of $-prefixed values during save() and update() operations in the Django MongoDB backend.
Values like {"$concat": [...]} or ("$name", "-", "$name") were previously interpreted as MongoDB operators instead of being stored literally.
The patch ensures that all such values are wrapped with $literal when appropriate, preventing unintended expression or operator injection.

Changes in this PR

  • Updated value compilation to wrap dict, tuple, string, and numeric values in $literal where needed.
  • Update SQLUpdateCompiler to wrap any direct value (i.e. string, dict, tuple, etc).

Test Plan

  • Added unit tests verifying that $-prefixed values are safely stored and not interpreted as expressions.
  • Tested scenarios for dicts, tuples, and Value() wrappers.
  • Verified that regular (non-$-prefixed) values are unaffected (basic unit test from Django)

Checklist

Checklist for Author

  • Did you update the changelog (if necessary)?
  • Is the intention of the code captured in relevant tests?
  • If there are new TODOs, has a related JIRA ticket been created?

Checklist for Reviewer @Jibola @aclark4life @timgraham

  • Does the title of the PR reference a JIRA Ticket?
  • Do you fully understand the implementation?
  • Have you checked for spelling & grammar errors?
  • Is all relevant documentation (README or docstring) updated?

Focus Areas for Reviewer

Pay attention to the logic handling $literal wrapping in value compilation.

@WaVEV WaVEV marked this pull request as draft November 12, 2025 16:35
@timgraham

This comment was marked as resolved.

@WaVEV

This comment was marked as resolved.

Comment on lines +886 to +887
if is_direct_value(value):
prepared = {"$literal": prepared}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is it necessary to wrap all values? Are there problems with other types? Our logic in Value() only wraps list/int/str.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

A unit test (test_update_dollar_prefix_in_value_expression) shows the issue. Because we could update passing an expression, and the expression could be Value("$update")

Copy link
Collaborator

@timgraham timgraham Nov 12, 2025

Choose a reason for hiding this comment

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

What I meant: Is it necessary to wrap all direct values?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

understood, 🤔 . Will try to make a test and try to inject something.

Copy link
Collaborator Author

@WaVEV WaVEV Nov 13, 2025

Choose a reason for hiding this comment

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

well, 🤔. maybe only string should be wrapped, not any kind of value.

EDIT
it is possible:

def test_update_dict(self):
        obj = Author.objects.create(name="foobar")
        obj.name = Value({"$concat": ["$name", "-", "$name"]})
        obj.save()
        obj.refresh_from_db()
        self.assertEqual(obj.name, {"$concat": ["$name", "-", "$name"]})

this tails fails and the name was update as foobar-foobar
I think we should scape any kind of literal

Copy link
Collaborator

Choose a reason for hiding this comment

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

That seems sufficient to me.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Another case:

def test_update_dictvalue(self):
        obj = Author.objects.create(name="foobar", data={})
        obj.data = {"$concat": ["$name", "-", "$name"]}
        obj.save()
        obj.refresh_from_db()
        self.assertEqual(obj.data, {"$concat": ["$name", "-", "$name"]})

If the field is a JSONField. Any kind of type that could be a MongoDB query, could be exploitable.
I think wrapping al values will bring us a safe net.

@timgraham timgraham changed the title Fix handling of $-prefixed string values during save INTPYTHON-827 Fix Model.save() updates and QuerySet.update() for $-prefixed strings Nov 12, 2025
@WaVEV WaVEV force-pushed the INTPYTHON-827-Cannot-save-CharField-and-TextField-with-values-starting-with-$ branch 2 times, most recently from 68e4632 to c1b41ac Compare November 13, 2025 01:16
@WaVEV WaVEV marked this pull request as ready for review November 14, 2025 01:34
@WaVEV WaVEV force-pushed the INTPYTHON-827-Cannot-save-CharField-and-TextField-with-values-starting-with-$ branch from 51c8912 to 9ddb1ca Compare November 14, 2025 01:35
# appears in $project.
if isinstance(value, (list, int, str, dict, tuple)) and as_expr:
# Wrap lists, numbers, strings, dict and tuple in $literal to avoid ambiguity when Value
# is used in queries' aggregate or update_many's $set.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Out of curiosity, you replaced $project with aggregate. Is there another place in aggregate besides $project where this could be an issue?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, in place like $group or $match (for having or filter). Do we need a test for that?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think so, just wondering.

)
prepared = field.get_db_prep_save(value, connection=self.connection)
if hasattr(value, "as_mql"):
if is_direct_value(value):
Copy link
Collaborator

Choose a reason for hiding this comment

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

I still question whether the $literal escaping is overly broad. Better safe than sorry, sure, but it decreases readability a bit and makes queries larger. But I'm not sure how to compare the tradeoffs between some more complicated logic (isinstance() CPU time) and leaving it as is. It seems if we did wrapping of only the types that Value() wraps, it should be safe, unless the logic in Value() is deficient. And are there any string values besides those that start with $ that could be problematic? Perhaps to be discussed in chat tomorrow.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

🤔 I agree. Seeing $literal everywhere is annoying, but Value's resolver is probably covering the most common types used in queries. On the other hand, implementing the same escaping logic that Value uses is not a big deal.

@WaVEV WaVEV force-pushed the INTPYTHON-827-Cannot-save-CharField-and-TextField-with-values-starting-with-$ branch from 9ddb1ca to cb67abe Compare November 14, 2025 02:27
Copy link
Collaborator

@timgraham timgraham left a comment

Choose a reason for hiding this comment

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

Comments to explain a few of my edits.

@timgraham timgraham force-pushed the INTPYTHON-827-Cannot-save-CharField-and-TextField-with-values-starting-with-$ branch from e8b1396 to 52f0d44 Compare November 16, 2025 01:29
@timgraham timgraham changed the title INTPYTHON-827 Fix Model.save() updates and QuerySet.update() for $-prefixed strings INTPYTHON-827 Prevent Value strings and model update values from being interpreted as expressions Nov 16, 2025
@timgraham timgraham force-pushed the INTPYTHON-827-Cannot-save-CharField-and-TextField-with-values-starting-with-$ branch from 52f0d44 to 215d3be Compare November 16, 2025 01:32
@timgraham timgraham merged commit 215d3be into mongodb:main Nov 17, 2025
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants