Django: require_POST (question post mentorat :))
Re ! :)
Donc voici les vues :
@require_POST
@login_required
def enigma_view(request, pk, id):
game_character = CharacterUser.objects.get(pk=pk)
enigma = Enigma.objects.get(id=id)
user_enigma, _ = UserEnigma.create_for_game_character(enigma=enigma, game_character=game_character)
form = UserEnigmaForm()
return render(request, "rpg/enigma.html", context={"user_enigma": user_enigma, "form": form})
@require_POST
@login_required
def enigma_form_validation(request, id):
user_enigma = UserEnigma.objects.get(id=id)
game_character = user_enigma.game_character
story = user_enigma.branch.story
form = UserEnigmaForm(request.POST, instance=user_enigma)
if form.is_valid():
instance = form.save()
if instance.user_choice == instance.good_choice:
instance.enigma_completed(request, game_character)
else:
instance.enigma_lost(request)
return redirect("rpg:branch", pk=game_character.pk, story_id=story.story_id)
Si dans la première vue je mets un require_POST c'est parce que pour moi je modifie l'état de la BDD. Donc je suis obligé de passer par une requête POST. Et je n'y accède que par une POST à cette vue.
Depuis quelques projets j'applique ce principe de, je touche potentiellement à la BDD ==> décorateur require_POST d'office.
@classmethod
def create_for_game_character(cls, enigma, game_character):
return cls.objects.get_or_create(
title=enigma.title,
text=enigma.text,
good_choice=enigma.good_choice,
weapon_to_win=enigma.weapon_to_win,
indication=enigma.indication,
branch=enigma.branch,
game_character=game_character,
illustration=enigma.illustration
)
Merci Thibault !
PS
En bonus le résultat : Juste ici ^^
EDIT
Sur le même principe avec le projet du mois quand j'ajoute au panier : je modifie l'était de la BDD donc POST obligatoire.
@require_POST
@login_required
def add_to_cart(request, pk):
user = request.user
garment = get_object_or_404(klass=Garment, pk=pk)
cart, _ = Cart.objects.get_or_create(user=user)
# Vérifier d'abord si dans le panier de l'utilisateur avec message
if cart.orders.filter(garment__id=garment.id).exists():
messages.add_message(request, messages.WARNING, f"{garment.description} est déjà dans votre panier")
return redirect(garment)
# Vérifier sinon si dans un panier tout court avec message (mais différent que le précédent)
elif Cart.objects.filter(orders__garment__id=garment.id).exists():
messages.add_message(request, messages.WARNING, f"{garment.description} est déjà dans un panier")
return redirect(garment)
# Sinon ajouter au panier
else:
order = Order.objects.create(user=user, garment=garment)
cart.orders.add(order)
return redirect("shop:cart")
Salut Gab !
Du coup pour expliquer un peu mieux ce que je disais l'autre jour.
Le require_POST va effectivement n'autoriser que les requêtes POST vers ta vue. Si tu fais une requête GET, tu te retrouveras avec une erreur.
Imagine que dans ton application Django tu aies une URL du style /delete_user/<user_id>. Cette URL, lorsqu'elle est visitée, supprime l'utilisateur dont l'ID est passé dans l'URL.
Un utilisateur (ou un tiers malveillant) pourrait accidentellement ou intentionnellement visiter cette URL et supprimer l'utilisateur.
Imagine par exemple un simple robot de crawling de Google qui trouve cette URL dans ton sitemap, et que tu ne l'a pas exclu avec le robots.txt, tu pourrais avoir un crawler qui supprime un utilisateur car il fait une requête GET vers cette URL (il faudrait qu'il utilise un ID spécifique aussi, donc c'est peu probable mais imaginons).
Un crawler, ou quelqu'un qui visite ton site et entre une URL dans la barre d'adresse, fera une requête GET. En mettant le require_POST, tu empêches donc ce problème.
Mais... ce que je te disais en mentorat l'autre jour, c'est que si ton application permet de supprimer un utilisateur juste en visitant une URL, alors le problème n'est pas que tu ne limites pas les requêtes à GET ou POST.
Parce que quelqu'un qui s'y connait un peu pourrait facilement envoyer une requête POST avec un outil comme Curl ou Postman et supprimer l'utilisateur si ta vue n'est pas correctement sécurisée.
C'est pour ça qu'il faut faire attention et bien comprendre ce qu'il se passe. La seule chose que va faire le require_POST c'est de retourner une erreur si tu accèdes à l'URL avec une requête GET. Rien de plus.
Ça peux éviter le cas de figure que je t'ai donné plus haut, mais ce cas de figure ne devrait pas se passer si ta vue est correctement sécurisée. Je me répète mais si n'importe qui peut supprimer un utilisateur ou modifier ta base de données simplement en visitant une URL ou en faisant une requête POST bien préparée sur ton serveur, le problème est dans la sécurisation de ta vue plus que dans la limitation des requêtes GET ou POST.
Dans le cas d'une vue qui supprimerait l'utilisateur, il faudrait t'assurer que la personne qui a initié la requête est bien connectée à ton site, qu'elle a les permissions nécessaires, potentiellement dans le cas d'une suppression d'utilisateur qu'elle a confirmé son mot de passe ou d'autres informations qu'elle seule connaisse pour vérifier son identité, etc. Des choses du coup qu'un crawler ou un tiers malveillant ne pourrait pas trouver et envoyer avec sa requête POST.
J'espère que c'est plus clair avec ces explications :)
super merci Thibault !
j'ai tout compris. j'ai relu 2 fois et nikel c'est compris ! :)
mais pour cas ci-dessus pour moi pas de problème de sécurité ?
en plus de passer par une POST j'ai un login required.
donc pour moi pas de soucis pour ce cas particulier ?
en tous cas je pense que tout est un peu plus clair dans la tête. Je commence à me poser des questions sur la sécurité. Même si Django gère énormément de choses.
EDIT
Ok je pense avoir une idée de ce que je devrais faire pour un max de sécurité.
La premiere vue :
@require_POST
@login_required
def enigma_view(request, pk, id):
game_character = CharacterUser.objects.get(pk=pk)
enigma = Enigma.objects.get(id=id)
user_enigma, _ = UserEnigma.create_for_game_character(enigma=enigma, game_character=game_character)
form = UserEnigmaForm()
return render(request, "rpg/enigma.html", context={"user_enigma": user_enigma, "form": form})
CharacterUser a une ForeignKey vers AUTH_USER_MODEL.
Du coup est-ce que je ne devrais pas faire un truc du genre :
if game_character.user != request.user:
# Je refuse l'accès à la vue !
Tu penses que j'ai raison ?
Merci ^^
Salut Gab !
Le @require_POST et le @login_required sont déjà une bonne base pour sécuriser ta vue, vu qu'ils s'assurent que la requête est un POST et que l'utilisateur est connecté.
Mais oui, ton intuition est bonne : même si l'utilisateur est connecté, tu veux t'assurer qu'il a le droit de faire des actions sur le CharacterUser spécifique. Du coup, ta petite vérification supplémentaire est la bonne idée pour t'assurer que l'utilisateur connecté est bien celui qui a le droit de jouer avec ce personnage !
if game_character.user != request.user:
# Rediriger ou afficher une erreur !
Avec cette vérification, tu te protèges contre le scénario où quelqu'un se connecte et essaie de jouer avec un personnage qui n'est pas le sien.
Le mieux quand tu fais ça c'est d'essayer de te mettre dans la peau de quelqu'un qui souhaiterait attaquer ton site. Dans ce cas-là, essaie de voir sans cette vérification si tu arrives à modifier ta base de données en faisant une requête POST avec les bonnes informations et un pk différent de celui de l'utilisateur connecté.
Si tu veux aller un peu plus loin, tu pourrais retourner une erreur 403 (Forbidden), qui indique que l'accès est refusé. Django a une shortcut pour ça, permission_denied:
from django.core.exceptions import PermissionDenied
if game_character.user != request.user:
raise PermissionDenied
merci Thibault ! je vais suivre ces principes !
ok donc je vais deja faire ca sur cette vue. et regarder les autres puis appliquer ça sur les projets.
le denied tu vas voir je l'utilise dans le projet du mois ! :) Car je ne veux pas qu'un utilisateur puisse accéder aux tickets sav si ce ticket n'est pas le sien.
si c'est ok on peut clôturer !
merci car ce soir j'ai l'impression d'avoir avancé avec Django ! je vais m'obliger à me poser les bonnes questions pour proteger les vues !
Salut salut ! :)
Je re ouvre la question
@require_POST
@login_required
def enemy_attack_view(request, story_id):
user = request.user
story = Story.objects.get(story_id=story_id)
game_character: CharacterUser = CharacterUser.objects.get(user=user, story=story)
enemy = game_character.enemyuser
# Mes actions ...
return redirect(...)
Un autre exemple. Ici je récupère l'histoire dans l'url.
Pour l'instant je n'autorise pas le joueur à jouer plusieurs histoires, donc c'est impossible q'un joueur possède plusieurs personnages.
Mais imaginons que nous ayons rien que deux histoires...
Je suis en train de jouer, et je décide de mettre dans mon url l'id d'une autre histoire. Ici ça va me récupérer mon autre personnage ?
C'est un problème pour toi ? Car on ne peut pas influer sur un autre joueur mais sur ses propres personnages. Donc c'est tant pis pour nous j'ai envie de dire.
J'ai bien compris ou j'ai tord ?
Tu metterais quelque chose en place ici ?
Je suis un peu chiant avec ça mais je reprends toutes les vues de NetRPG et j'essaye de penser à ce que tu as dis.
Merci d'avance
EDIT :
@login_required
def next_branch(request, pk, story_id):
# StoryCompleted CharacterUser** Story**
user = request.user
choice = Choice.objects.get(pk=pk)
# Comment sécuriser le fait que le pk ne doit pas être modifié
J'étais en train de sécuriser mes vues et je suis tombé sur celle-ci.
Ici je récupère le pk du choice dans l'url.
Mais par contre je ne vois pas comment je peux sécuriser ça... ça fait 40 min que je suis dessus, je me suis dit avec un paramètre d'url mais je ne pense pas.
Une idée ?
Je pense que si j'arrive à sécuriser cette vue je peux me débrouiller pour pas mal de cas, car pour les précédents j'ai réussi à mettre en place des PermissionDenied où il fallait.
Merci d'avance
Salut Gab et désolé pour le délai de réponse.
👉 "Pour l'instant je n'autorise pas le joueur à jouer plusieurs histoires, donc c'est impossible q'un joueur possède plusieurs personnages."
Dès que tu as une phrase qui commence par "Pour l'instant", tu dois te dire que ce n'est pas une bonne raison pour ne rien faire 😉
Tu dois à chaque étape du développement te demander ce que l'utilisateur a le droit ou non de faire, et empêcher le reste d'être effectué (et tester le comportement de la sorte dans tes tests).
C'est le même raisonnement qui peut te conduire à ne pas sécuriser une vue correctement parce que dans ton formulaire tu ne donnes pas la possibilité d'envoyer une information spécifique. Jusqu'au jour où finalement tu te décides à rajouter cette possibilité, en oubliant de sécuriser ta vue, et tu viens d'introduire une faille de sécurité.
Donc dans tous les cas, il faut sécuriser tes vues et tes données, peu importe ce que tu as prévu ou non de faire.
Concernant ta vue next_banch, est-ce que le Choice est relié à un utilisateur ? Si oui, tu peux vérifier que le user de ton Choice est bien le même que request.user.
Par exemple, si un Choice est associé à une Story et que l'utilisateur doit être l'auteur de cette Story, tu peux utiliser get_object_or_404 :
choice = get_object_or_404(Choice, pk=pk, story__user=request.user)
Ok Merci Thibault ! J'ai revenir sur tout ça pour refaire des vérifs mais pour moi la plupart des vues sont sécurisés j'avais bien rebossé dessus.
Et pour le choice la seule chose qui est embêtante c'est que le joueur peut soit tricher, soit au pire ça le fera boguer. Mais nan le choice n'a aucun rapport avec l'utilisateur quelque part... ça ne met pas non plus en cause la sécurité, juste la possibilité de tricher.
là pour le coup ça me parait compliqué à vérouiller... :s
Inscris-toi
(c'est gratuit !)
Tu dois créer un compte pour participer aux discussions.
Créer un compte