How it arises in Django
import_string() and friends. django.utils.module_loading.import_string(dotted_path) imports a module and returns the attribute named by the last segment. Settings use it everywhere (MIDDLEWARE, AUTHENTICATION_BACKENDS, storage backends), and that is fine: settings are written by the developer. The same helper called with a value from a request, a database row or an uploaded configuration file lets the sender import any module on the path, and importing a module runs its top-level code.
Dispatch by name. A view reads request.POST["action"] and calls getattr(self, action)(). It works for the three intended actions and for every other method on the class and its bases, including ones that delete.
Assignment by name. The mirror image: for key, value in request.POST.items(): setattr(obj, key, value). The form posts display_name; nothing stops a client from adding is_staff. This is the unsafe reflection end of the class. It writes attributes instead of running text, and on a model instance followed by save() the effect is a privilege change.
eval() in back-office tools. Pricing rules, report formulas and feature-flag conditions stored as Python expressions and evaluated per request. They sit behind the admin, which is treated as trusted until one staff account is phished.
Vulnerable and fixed
A profile endpoint that updates "whatever fields were sent" and runs a bulk action by name:
from django.http import JsonResponse
def update_profile(request):
profile = request.user.profile
for key, value in request.POST.items():
setattr(profile, key, value)
profile.save()
action = request.POST.get("action")
if action:
getattr(profile, action)()
return JsonResponse({"ok": True})
Django's own answer is a ModelForm with an explicit fields list: only those attributes can be written, and they are validated on the way in. Actions go through a dictionary of callables, so the request selects among functions the developer listed:
from django import forms
from django.http import JsonResponse
from myapp.models import Profile
class ProfileForm(forms.ModelForm):
class Meta:
model = Profile
fields = ["display_name", "timezone"]
ACTIONS = {"resend_welcome": lambda p: p.resend_welcome()}
def update_profile(request):
form = ProfileForm(request.POST, instance=request.user.profile)
if not form.is_valid():
return JsonResponse({"errors": form.errors}, status=400)
profile = form.save()
action = ACTIONS.get(request.POST.get("action"))
if action:
action(profile)
return JsonResponse({"ok": True})
What Django already does for you
ModelForm has a fields attribute, and the documentation strongly recommends setting it explicitly, because a form that picks up new model fields automatically can let a user set them without anyone noticing. The admin registers actions explicitly. apps.get_model() resolves only installed models, which is a far narrower lookup than a general import. Django's template language cannot call arbitrary Python. Used as intended, the framework keeps names chosen by the developer and values chosen by the user apart.
What es-python adds
Name-based flows are hard to review because the dangerous line contains no dangerous-looking function. es-python reports them by what happens: a name that came from the request was used to write an attribute, look one up, import a module or compile code.
- A request-supplied name passed to
setattr()is reported assetattr, taggedunsafe reflection. A write onto a module or a class, which replaces code, is reported asmonkey patch. getattr()with a request-supplied name is reported asgetattr. Most such calls are serializers reading a field, so EyalSec keeps the event only when the name reaches an introspection attribute such as__class__or__globals__, and suppresses the rest.eval,execandcompileon request data carry therceimpact tag. An import by computed name is reported as__import__; a name passed toimportlib.import_module()is reported where the import system looks that name up on disk.
The fixed view produces none of these: the form writes fields named in source, and the dictionary lookup involves no reflection call. With data from the network the code-running labels are graded Critical, and under the Block network code execution template eval and exec raise a RuntimeError before anything is compiled.
Review checklist
- Search for
import_string(,import_module(,getattr(andsetattr(whose name argument is not a literal. - Replace attribute loops with a
ModelFormor a serializer that lists its fields. - Dispatch through dictionaries of callables.
- Move stored formulas to a small expression parser, not
eval(). - Treat admin-entered code as untrusted input.
References
- Reference: Django documentation: Django Utils, import_string() (checked 2026-10-12)
- Reference: Django documentation: Applications, apps.get_model() (checked 2026-10-12)
- Reference: Django documentation: Creating forms from models, selecting the fields to use (checked 2026-10-12)