(How) does Django prevent data injection by manipulating forms? - django

I'm using Django's built-in User model in one of my projects. Users should be editable, of course. Since it's the most convenient solution I started providing the form for User edits by using Django's own UserChangeForm. UserChangeForm is basically a ModelForm of the UserModel, so it makes it possible to change all the fields of the User model.
I don't want users to be able to change every field. So, my first idea was to pass the whole UserChangeForm into the template but render only the fields that I need (say 'username' and 'email'). I also want only superusers to be able to change a username, so this field is only rendered if you are a superuser.
The essential code looks like that:
view function
def edit_user(request, pk):
... #code to ensure not everyone can edit every user
user = User.objects.get(pk=pk)
if request.method == 'POST':
form = UserChangeForm(request.POST, instance=user)
if form.is_valid:
form.save()
... #redirect
else:
form = UserChangeForm(instance=user)
... # render template
form in the template
<form action="{{ request.path }}" method="post">
{% csrf_token %}
{% if user.is_superuser %}
{{ form.username }}
{% endif %}
{{ form.email }}
<button type="submit">Save</button>
</form>
Now, my question is: How about the security aspects of this solution? I do nothing to prevent an attacker to add the e.g. the username field even if he is no superuser. By that, he would populate the POST data with additional data, which is then send to the view function and used to update the User object. That could get really dangerous, since the original UserChangeForm also contains a field 'is_superuser'.
I tried the hack the form myself to test my suspicion. I logged in as a normal user, added the username input via the Developer Tools and submitted the form. The result was an exception:
Traceback:
File "/Library/Python/2.7/site-packages/django/core/handlers/base.py" in get_response
111. response = callback(request, *callback_args, **callback_kwargs)
File "/Library/Python/2.7/site-packages/django/contrib/auth/decorators.py" in _wrapped_view
20. return view_func(request, *args, **kwargs)
File "/Users/joker/Development/wiki2099/wiki2099/apps/accounts/views.py" in edit_user
69. form.save()
File "/Library/Python/2.7/site-packages/django/forms/models.py" in save
364. fail_message, commit, construct=False)
File "/Library/Python/2.7/site-packages/django/forms/models.py" in save_instance
74. " validate." % (opts.object_name, fail_message))
Exception Type: ValueError at /accounts/edit/12/
Exception Value: The User could not be changed because the data didn't validate.
I'm not sure if that means that this kind of attack is not possible or that I just didn't do it right. I think, the CSRF token could prevent such kind of hacks, but I found nothing about that matter in the docs. Could anyone enlighten me? Is there any mechanism to prevent attackers from using not-rendered form fields and how does it work?
Thanks in advance!

If you want to restrict the fields that the user can edit, then you need to define a form with a subset of the model's fields.
If you don't render a form field in the template, but the user submits data for it, then Django will process it as normal. Having looked at the traceback, I don't understand why your attempt failed, but an attack using the method you described is possible.
The csrf protection won't help you here. Its purpose is to prevent a third party tricking your users into submitting data to your site, not protect against hand crafted POST data with extra fields.
There's another issue to be aware of if you deliberately don't render fields in the template: If the omitted field is not required, then the missing POST values will be interpreted as empty strings, validate, and your data will be wiped.

As far as I know, CSRF token only protects from CSRF atacks and doesn't do any things about permissions to change data.
Though it isn't clear if you only not render the field in case the user is not a superuser, or you didn't add it to self.fields at all. You should use the second way, or even have a separate form for superusers.

Related

Forms, Model, and updating with current user

I think this works, but I came across a couple of things before getting it to work that I want to understand better, so the question. It also looks like other people do this a variety of ways looking at other answers on stack overflow. What I am trying to avoid is having the user to have to select his username from the pulldown when creating a new search-profile. The search profile model is:
class Search_Profile(models.Model):
author_of_profile = models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.CASCADE,blank=True)
keyword_string = models.CharField(max_length=200)
other_stuff = models.CharField(max_length=200)
The form I ended up with was:
class Search_Profile_Form(ModelForm):
class Meta:
model = Search_Profile
fields = [ 'keyword_string', 'other_stuff']
Where I deliberately left out 'author_of_profile' so that it wouldn't be shown or need to be validated. I tried hiding it, but then the form would not save to the model because a field hadn't been entered. If I was o.k. with a pulldown I guess I could have left it in.
I didn't have any issues with the HTML while testing for completeness:
<form action="" method="POST">
{% csrf_token %}
{{ form.author_of_profile}}
{{ form.keyword_string }}
{{ form.other_stuff }}
<input type="submit" value="Save and Return to Home Page">
</form>
And the View is where I ended up treating the form and the model separated, saving the form first, then updating the model, which is where I think there might be some other way people do it.
def New_Profile(request):
if request.method=='POST':
form = Search_Profile_Form(request.POST)
if form.is_valid():
post=form.save(commit=False)
# here is where I thought I could update the author of profile field somehow with USER
# but If I include the author_of_profile field in the form it seems I can't.
post.save()
#So instead I tried to update the author_of profile directly in the model
current_profile=Search_Profile.objects.last()
current_profile.author_of_profile=request.user
current_profile.save()
return(redirect('home'))
else:
form=Search_Profile_Form()
return render(request, 'mainapp/New_Profile.html', {'form': form})
So a few questions:
For the Foreign Key in author_of_profile field, is it better to use blank=True, or null=True
I ended up using request.user rather than importing from django.contrib.auth.models import User is there any difference?
My real question though, is the above a reasonable way to get form data and update the database with that data and the user? Or am I missing some other way that is more build in?
post=form.save()
current_profile.author_of_profile=request.user
post.save()
return(redirect('home'))
try something like this. save the form to db then change the author again. save(commit=False) will not save the date to db immediately.

how to pre-fill an HiddenInput in admin ModelForm

I'm sure that problems similar to the one I'm going to ask about were already discussed somewhere, but I always found very tricky solutions while I think there must be a very straightforward one (you can guess I'm a totally newbie).
I have a model (Lecturer) that is connected OneToOne to the User model:
class Lecturer(models.Model):
user = models.OneToOneField(User)
... other fiels follow ...
I'd like each user to create its own Lecturer object through the admin site.
My idea is to present to the user the add_view without the user field. Then I would something like obj.user=request.user when saving the model.
In other words, I don't want to give the user the possibility of selecting a different user among the registered for its own Lecturer object.
I modified the form by overriding the get_form method and by providing a custom form:
admin.py
class LecturerAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if is_lecturer(request.user):
kwargs['form'] = UserLecturerForm
return super(LecturerAdmin, self).get_form(request, obj, **kwargs)
class UserLecturerForm(forms.ModelForm):
class Meta:
model = Lecturer
fields = ('__all__')
widgets = {'user': forms.HiddenInput()}
I cannot just exclude the user field and give it a value at some other level (e.g. save_model or clean ...) because this raises an error at the template rendering level:
Django Version: 1.7.7
Exception Type: KeyError
Exception Value: u"Key 'user' not found in 'LecturerForm'"
Exception Location: /usr/lib/python2.7/dist-packages/django/forms/forms.py in getitem, line 147
Python Executable: /usr/bin/python
Python Version: 2.7.9
Error during template rendering
In template /usr/lib/python2.7/dist-packages/django/contrib/admin/templates/admin/includes/fieldset.html, error at line 7
[...]
7 <div class="form-row{% if line.fields|length_is:'1' and line.errors %} errors{% endif %}{% if not line.has_visible_field %} hidden{% endif %}{% for field in line %}{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% endfor %}">
I tried making the user field hidden in the form. But then the problem came of how to give it the correct request.user value, given that the form doesn't know anything about the request. If I don't fill it with a valid value, the form will not validate (and I cannot use solutions involving the save_model as suggested here)
I found solutions that involve changing the view, but I wouldn't do it in the admin context.
An alternative would be to change the validation behavior, but still validation is a method of the form and the form doesn't know request.
Any help is appreciated.

django-allauth redirects again to signup/login forms

I am using django-allauth for local account management. I have customized templates ,login.html and signup.html. Both of these templates are placed in templates/account/ dir and both are accessible properly.
site root i.e localhost:8000 points to index.html which includes using {% include%} both the templates on main page.
form action for signup form in signup.hmtl is set to action="{% url 'account_signup' %}" and that of login.html is set to "{% url 'account_login' %}"
Both the templates appears OK on the main page. The problem arises when I try to use these forms for sigin/login. Instead processing the POST for signup or login I am redirected to locahost:8000/accounts/signup/ for signup and localhost:8000/accounts/login/ for login. I guess I am using the right urls that is account_signup and account_login
I have all settings for allauth. Is this is the default behaviour or I'm missing some thing out? Thanking in anticipation
Well I managed to get rid of the problem after spending some hours. Just in case that some one else caughtup in the same situation I would like to share my solution. The problem was forms have been instantiated by View by using prefix SignUpForm(prefix=signupform) and for that reason allauth class AjaxCapableProcessFormViewMixin(object) (which I don't know how it works) was unable to get data from the fields and its if form.is_valid() was always false as the form has error messages dictionary was containing this field is required for all the fields in the form. Jus to test I removed Prefixing from the form instantiation and it worked but make me feel strange as these kind of hidden errors can take upto indefinite time to resolve especially for some niwbie like me
class AjaxCapableProcessFormViewMixin(object):
def post(self, request, *args, **kwargs):
form_class = self.get_form_class()
form = self.get_form(form_class)
if form.is_valid():
response = self.form_valid(form)
else:
response = self.form_invalid(form)
return _ajax_response(self.request, response, form=form)

Django user injection in model properties

I have this models in Django:
News
Comments
Reactions
Relations are:
a News has various Comments
a Comment has various Reactions
The problem is the user (in request / session): the user may subscribe to a reaction, or a comment; he may be logged in or not. (it's a foo example, it doesn't have much sense)
I can't do in template:
{% for reaction in this_news.comments.reactions %}
{{ reaction.name }}
{% if reaction.user_subscribed %} #reaction.user_subscribed(request.user)...
You have subscribed this reaction!
{% endif %}
{% endfor %}
Problems are:
I can't call the method in the template with a parameter (see the comment above)
Models don't have access to request
Now i'm calling an init_user method in News Model, passing the request. Then i have the same method in Comment and Reaction model, and i have to set the user_subscribed property cycling the children of each model.
Isn't there a smarter way to do this?
EDIT: thanks to the Ignacio's hint about using custom tag i'm trying to do a generic mode to pass the user (avoiding the use of closures because i don't know how to use them atm):
def inject_user(parser, token):
try:
# split_contents() knows not to split quoted strings.
tag_name, method_injected, user = token.split_contents()
except ValueError:
raise template.TemplateSyntaxError("%r tag requires exactly three arguments" % token.contents.split()[0])
return InjectUserNode(method_injected, user)
class InjectUserNode(template.Node):
def __init__(self, method_injected, user):
self.method_injected = template.Variable(method_injected)
self.user = template.Variable(user)
def render(self, context):
try:
method_injected = self.method_injected.resolve(context)
user = self.user.resolve(context)
return method_injected(user)
except template.VariableDoesNotExist:
return ''
When i use it {% inject_user object.method_that_receives_a_user request.user %} i come to this error 'str' object is not callable in method_injected(user); how i can fix that?
Write custom template tags that take the user and set a context variable to indicate presence or absence of the criterion.
I've resolved it in a less elegant way, but it worked for me.
I've created a sort of singleton in my User defined class, with a property that i set in every view i need it.
The property is User.current.
Then, inside the models, where i need that i get the current user looking in User.current.

Django Admin - saving data from custom form

I need to customize the admin panel added to the model page related inputs from another model​​. But I can not figure out how to save them.
admin.py
class OrderAdmin(admin.ModelAdmin):
change_form_template = 'admin/testapp/order/change_form.html'
def change_view(self, request, object_id, extra_context=None):
order = Order.objects.get(id=object_id)
card_list = Card.objects.all().filter(type=order.type)
result = super(OrderAdmin, self).change_view(request, object_id, extra_context={
'card_list': card_list,
})
return result
change_form.html
{% for card in card_list %}
<input type="text" name="card-{{ card.id}}" value="{{ card.qty }}"></td>
{% endfor %}
How to save the changed values ​​in the Card model?
I tried to do as described here:
https://docs.djangoproject.com/en/1.3/ref/contrib/admin/#adding-custom-validation-to-the-admin
But self.cleaned_data does not include my data from inputs.
Thanks.
UPD: Well, I caught the data, but I think it's pretty messy way.
I'm hardly can imagine how I would calculate the id from inputs.
def save_model(self, request, obj, form, change):
request.POST['card-288']
Saving should be done in save_model. Your card_list data should also be would have been available in the form parameter if you had used django forms. Although you could still access it through request as you correctly pointed out.
You will also have to sanitize the data yourself, something that django forms does for your automatically.
You are getting exactly what you produced :), nothing unexpected there!
You must override clean (take a look here for instance) if you want some fields that you manually added in the change_list to be taken care of.
If you go that way, to retrieve the ID is up to you, but a simple split('-')[-1] would work.
This answers your specific need.
On the other side, I think your are not going the right way. The django admin is good enough to deal with ForeignKeys ManyToMany, so I see no need to do things yourself.
While you can get it working with the above hint, I suggest you start all over - if you want, a new SO question where you publish the initial Model and what exactly you are trying to achieve.