Check template variables in a string

I have a model field where the user can enter a template variable as part of the text. For example: “{{ first_name }}, here is your order confirmation”. I want to verify that only allowed variables are part of the text. I thought I could override the field’s clean method in the form and test render the text and throw an exception if the user includes a variable that is not allowed.

class EmailForm(forms.ModelForm):
    class Meta:
        ...
        
    def clean_subject(self):
        subject_cleaned = self.cleaned_data.get('subject')
        try:
            template = Template(subject_cleaned)
            first_name = "First"
            last_name = "Last"
            context = Context({'email': 'test@test.com',
                               'first_name': first_name,
                               'last_name': last_name,
                               'full_name': f"{first_name} {last_name}",
                               })
            template.render(context)  # I want this to error if variable is incorrect.
            return subject_cleaned
        except Exception as e:
            raise forms.ValidationError("Incorrect variable tag.")

This doesn’t work as any incorrect variable is ignored and the template string is rendered without it. I understand that is the default behavior as you don’t want a error like that in production. I, however, need to give the user feedback so they know they’ve entered an incorrect variable. Am I on the right track here?

I don’t understand what you’re saying / asking here. Are you saying that you’re going to have a form field, allowing someone to enter in text that will later be rendered as part of a template? (as opposed to entering data that will be rendered in a template as data)

Yes, it’s a form field that is part of a ModelForm. The text they enter will be stored and rendered later as a template.

Wow, that sounds like a massive security hole in the making. Being rendered as a template means you won’t have any protection against things like javascript code injections.

It’s a closed system with only a handful of employees able to access. I’m scrubbing everything and the end result is the subject line in an email. I appreciate the concern though. I had a feeling I might want to expound on the security questions that people might have, but that’s tangential to the original question. I would NEVER do this in a public facing site.

So it’s not just template variables to worry about, it’s also the addition of tags.

I wouldn’t use the Django template engine for something like this, I’d probably use something like Python format strings, which at least gives you much tighter controls over what’s being generated.
In other words, instead of having them enter {{ first_name }}, here is your order confirmation, I’d have them enter {first_name}, here is your order confirmation, and then use this field (let’s call it “subject”) as subject_text = subject.format(context), where context are the variables allowable in that field.

I’ve got tags covered. I omitted that for brevity. The email rendering/sending portion of the system already uses the django template system, so the end result has to be django style template variables. On the form I can dictate the variable style and use something like {first_name} and then convert it to a django variable before rendering. That still leaves me with the question of how to use Template.render() to make sure the variable is part of the context and throw an error if it isn’t.

I originally was thinking of just using regex to get the variable name(s) from the cleaned input and comparing that name to a list of allowed names. Maybe that’s the way to go. I just thought that there should be a simple way for Template to do it.

Thanks for your help Ken. I appreciate it.

Actually, I wasn’t thinking of you replacing your entire rendering process with format. I was thinking that you could just format that subject string and pass it as data within your context to the final render step. It gets you out of the way of trying to parse user input.

Ah, I see now. I’ve got them entering {first_name} and then I:

subject_formatted = subject_cleaned.format(email='{{ email }}',
                                            first_name='{{ first_name }}',
                                            last_name='{{ last_name }}',
                                            full_name='{{ full_name }}')

Works great! It throws an error just like I need. Thanks again for the help!