Rails: How to prevent unauthorized access of controller update action - ruby-on-rails

Hi I am new to rails and I am trying to figure out how to prevent unauthorized access to the update action of a controller.
I know I could have a before_filer that kicks out people that arent logged in and I have redirect_to in the edit action, but I want a way to stop a user from editing an object that does not belong to them.
Ex: A authorized user can simply change a job object in my app, by directly sending a PUT request with any job.id as a parameter and change any field they want.
Here is my controller:
def update
#job = Job.find(params[:id])
#job.update_attributes(params[:job])
redirect_to jobs_path
end
To try and fix this problem I tried to check in the update action if it the user was authorized and if they werent, i would redirect them to the index page.
def update
#job = Job.find(params[:id])
if #job.user.id != current_login
redirect_to jobs_path
end
#job.update_attributes(params[:job])
redirect_to jobs_path
end
But when I try to do this, rails gives me an error saying I can only have one redirect in an action.

Well, the straightforward fix to your immediate problem is to use flow control to make sure that only one redirect_to is ever reached on a single request, as many others have suggested.
However, that's not really how I'd solve your larger problem.
First, there are a lot of existing solutions for managing authorization, such as cancan or rolify. I'd look into those.
Second, I'd use a before_filter to block access, as you suggest. Something like:
before_filter :load_job, :only => [:show, :edit, :update, :delete]
before_filter :require_authorization, :only => [:edit, :update, :delete]
def load_job
#job = Job.find(params[:id])
end
def require_authorization
redirect_to jobs_path unless current_user.can_edit?(#job) # or whatever you want to check
end
The before filters will execute in order, so you'll already have the user & the job available when you check permissions, and can check permissions for that specific job.

def update
#job = Job.find(params[:id])
#job.update_attributes(params[:job]) unless #job.user.id != current_login
redirect_to jobs_path
end
:)

This is probably because after the first redirect the second one could still be executed.
Thus putting the update_attributes and the second redirect into the else path like this should solve the problem:
def update
#job = Job.find(params[:id])
if #job.user.id != current_login
redirect_to jobs_path
else
#job.update_attributes(params[:job])
redirect_to jobs_path
end
end

You can either do redirect_to jobs_path and return or return redirect_to jobs_path.
Try the following:
def update
#job = Job.find(params[:id])
if #job.user.id != current_login
redirect_to jobs_path and return
end
#job.update_attributes(params[:job])
redirect_to jobs_path and return
end

Use an Else Clause
The problem is that the redirect_to method doesn't end the current method; it just tells the controller to set some headers. In order to prevent this problem, you need to make sure that control doesn't "fall through" to the second redirect. One way to do this would be to put your alternative path into an else clause. For example:
def update
#job = Job.find(params[:id])
if #job.user.id != current_login
redirect_to jobs_path
else
#job.update_attributes(params[:job])
redirect_to jobs_path
end
end

Related

If i use redirect_to i'll get Redirected too much on browser

My goal is to redirect user to index path if true else show new path.
class PostsController < ApplicationController
before_action :check_condition, only: [:index, :new]
def check_condition
if true
redirect_to posts_path
else
redirect_to new_post_path
end
end
def index
#posts = Post.find()
end
def new
#new_post = Post.new(title: "test")
end
end
I keep getting error redirected too much on the browser when I go to index path or new path
At the moment, you redirect to post_path every time when the if-condition is true no matter if you are already on the post_path.
You only need to redirect unless you are already on the path to the expected method. That can be done by checking the current action_name:
def check_condition
if true
redirect_to posts_path unless action_name == 'index'
else
redirect_to new_post_path unless action_name == 'new'
end
end
You're running to an infinite loop of redirects.
You're adding check_condition as before_action. This means, every time you redirect to index or new pages, check_condition will run again without reaching index or new methods and redirecting again instead, and so on.
My suggestion is to write the condition checker in the view or in a helper method (to determine the path of the link in the first place), not in the controller.
Try this. You need to return after redirect.
def check_condition
if true
redirect_to posts_path && return
else
redirect_to new_post_path && return
end
end
Sorry but, what's the condition to check? If the user is logged in? I think your issue is probably too many redirect_to's. Try:
def check_condition
path = condition ? posts_path : new_post_path
redirect_to path
end

Can't overwrite the active admin default redirect path

I am using active admin in my application. In my controller, I have an action update with redirect_to function. But while updating, it threw me an error.
Render and/or redirect were called multiple times in this action. Please note that you may only call render or redirect, and at most once per action. Also note that neither redirect nor render terminates execution of the action, so if you want to exit an action after redirecting, you need to do something like redirect_to(...) and return.
def update
#user = User.find(params[:id])
#user.update
mailers.notify(#user).deliver
redirect_to user_path(#user)
end
I tried
only redirect_to
redirect_to() and return
but nothing works.
before_filter :only =>[:create,:update] do
if self.action_name.to_sym == :create
#user = User.new(params[:user])
else
#user = User.find(params[:id])
end
I fix it by adding this to my update action .This works for me fine.
def update
update!do |format|
format.html { redirect_to_user_path(#user)}
end
What is your purpose? Why you use mailers.notify(#user).deliver inside the update action?
Move the notification to after_update filter in your User model, smth like this:
after_update :send_notifications
def send_notifications
Mailer.notify(self).deliver
end
Change the update method to smth like this:
def update
super do |format|
redirect_to user_path(#user), notice: "Your notice message" and return if resource.valid?
end
end

how to only edit and destroy my own content only?

Have a basic blog (it's actually edgeguide's blog: http://edgeguides.rubyonrails.org/getting_started.html)
Then I integrated Devise into it. So, user can only log in and see their own information.
Now trying to change it somewhat.
I'd like the users to see all content, but only edit and destroy their own only.
Trying to use before_action filter like this:
`before_action :authorize, :only => [:edit, :destroy]`
And this is the authorize method that I wrote:
def authorize
#article = Article.find(params[:id])
if !#article.user_id = current_user.id then
flash[:notice] = "You are not the creator of this article, therefore you're not permitted to edit or destroy this article"
end
end
But it doesn't work. Everything acts as normal, and I can delete mine and everyone's else content.
How do I get it that I can destroy ONLY my own content, and not everyone's else?
Not using CanCan, nor do I want to.
Not sure if this is worth including or not, but originally when I had everyone see their own content, that was via create action:
def create
#article = Article.new(article_params)
#article.user_id = current_user.id if current_user
if #article.save
redirect_to #article
else
render 'new'
end
end
You're having several problems
first, look at that :
if !#article.user_id = current_user.id then
You're only using one = instead of == so you are doing an assignation that will evaluate to current_user.id
Also, in your condition, you're only setting a flash message but not doing anything to really prevent the user.
Here's a corrected version :
def authorize
#article = Article.find(params[:id])
unless #article.user_id == current_user.id
flash[:notice] = "You are not the creator of this article, therefore you're not permitted to edit or destroy this article"
redirect_to root_path # or anything you prefer
return false # Important to let rails know that the controller should not be executed
end
end

return redirect_to in private controller method

Preface: I'm using devise for authentication.
I'm trying to catch unauthorized users from being able to see, edit, or update another user's information. My biggest concern is a user modifying the form in the DOM to another user's ID, filling out the form, and clicking update. I've read specifically on SO that something like below should work, but it doesn't. A post on SO recommended moving the validate_current_user method into the public realm, but that didn't work either.
Is there something obvious I'm doing wrong? Or is there a better approach to what I'm trying to do, either using devise or something else?
My UsersController looks like this:
class UsersController < ApplicationController
before_filter :authenticate_admin!, :only => [:new, :create, :destroy]
before_filter :redirect_guests
def index
redirect_to current_user unless current_user.try(:admin?)
if params[:approved] == "false"
#users = User.find_all_by_approved(false)
else
#users = User.all
end
end
def show
#user = User.find(params[:id])
validate_current_user
#user
end
def new
#user = User.new
end
def edit
#user = User.find(params[:id])
validate_current_user
#user
end
def create
#user = User.new(params[:user])
respond_to do |format|
if #user.save
format.html { redirect_to #user, :notice => 'User was successfully created.' }
else
format.html { render :action => "new" }
end
end
end
def update
#user = User.find(params[:id])
validate_current_user
respond_to do |format|
if #user.update_attributes(params[:user])
format.html { redirect_to #user, :notice => 'User was successfully updated.' }
else
format.html { render :action => "edit" }
end
end
end
private
def redirect_guests
redirect_to new_user_session_path if current_user.nil?
end
def validate_current_user
if current_user && current_user != #user && !current_user.try(:admin?)
return redirect_to(current_user)
end
end
end
The authenticate_admin! method looks like this:
def authenticate_admin!
return redirect_to new_user_session_path if current_user.nil?
unless current_user.try(:admin?)
flash[:error] = "Unauthorized access!"
redirect_to root_path
end
end
EDIT -- What do you mean "it doesn't work?"
To help clarify, I get this error when I try to "hack" another user's account:
Render and/or redirect were called multiple times in this action.
Please note that you may only call render OR redirect, and at most
once per action. Also note that neither redirect nor render terminate
execution of the action, so if you want to exit an action after
redirecting, you need to do something like "redirect_to(...) and
return".
If I put the method code inline in the individual controller actions, they do work. But, I don't want to do that because it isn't DRY.
I should also specify I've tried:
def validate_current_user
if current_user && current_user != #user && !current_user.try(:admin?)
redirect_to(current_user) and return
end
end
If you think about it, return in the private method just exits the method and passes control back to the controller - it doesn't quit the action. If you want to quit the action you have to return again
For example, you could have something like this:
class PostsController < ApplicationController
def show
return if redirect_guest_posts(params[:guest], params[:id])
...
end
private
def redirect_guest_post(author_is_guest, post_id)
redirect_to special_guest_post_path(post_id) if author_is_guest
end
end
If params[:guest] is present and not false, the private method returns something truthy and the #show action quits. If the condition fails then it returns nil, and the action continues.
You are trying and you want to authorize users before every action. I would suggest you to use standard gems like CanCan or declarative_authorization.
Going ahead with this approach you might end up reinventing the wheel.
In case you decide on using cancan, all you have to do is add permissions in the ability.rb file(generated by rails cancan:install)
can [:read,:write,:destroy], :role => "admin"
And in the controller just add load_and_authorize_resource (cancan filter). It will check if the user has permissions for the current action. If the user doesnt have persmissions, then it will throw a 403 forbidden expection, which can be caught in the ApplicationController and handled appropriately.
Try,
before_filter :redirect_guests, :except => [:new, :create, :destroy]
should work.
This is because you are using redirect twice, in authenticate_admin! and redirect_guests for new, create and destroy actions.
"Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and at most once per action."
That's the reason of the error. In show method, if you are neither the owner of this account nor the admin, you are facing two actions: redirect_to and render
My suggestion is to put all of the redirect logic into before_filter

Rails: Keeping user spoofing checks DRY

In a fit of unoriginality, I'm writing a blog application using Ruby on Rails. My PostsController contains some code that ensures that the logged in user can only edit or delete their own posts.
I tried factoring this code out into a private method with a single argument for the flash message to display, but when I did this and tested it by editing another author's post, I got an ActionController::DoubleRenderError - "Can only render or redirect once per action".
How can I keep these checks DRY? The obvious approach is to use a before filter but the destroy method needs to display a different flash.
Here's the relevant controller code:
before_filter :find_post_by_slug!, :only => [:edit, :show]
def edit
# FIXME Refactor this into a separate method
if #post.user != current_user
flash[:notice] = "You cannot edit another author’s posts."
redirect_to root_path and return
end
...
end
def update
#post = Post.find(params[:id])
# FIXME Refactor this into a separate method
if #post.user != current_user
flash[:notice] = "You cannot edit another author’s posts."
redirect_to root_path and return
end
...
end
def destroy
#post = Post.find_by_slug(params[:slug])
# FIXME Refactor this into a separate method
if #post.user != current_user
flash[:notice] = "You cannot delete another author’s posts."
redirect_to root_path and return
end
...
end
private
def find_post_by_slug!
slug = params[:slug]
#post = Post.find_by_slug(slug) if slug
raise ActiveRecord::RecordNotFound if #post.nil?
end
The before filter approach is still an ok option. You can gain access to which action was requested using the controller's action_name method.
before_filter :check_authorization
...
protected
def check_authorization
#post = Post.find_by_slug(params[:slug])
if #post.user != current_user
flash[:notice] = (action_name == "destroy") ?
"You cannot delete another author’s posts." :
"You cannot edit another author’s posts."
redirect_to root_path and return false
end
end
Sorry for that ternary operator in the middle there. :) Naturally you can do whatever logic you like.
You can also use a method if you like, and avoid the double render by explicitly returning if it fails. The key here is to return so that you don't double render.
def destroy
#post = Post.find_by_slug(params[:slug])
return unless authorized_to('delete')
...
end
protected
def authorized_to(mess_with)
if #post.user != current_user
flash[:notice] = "You cannot #{mess_with} another author’s posts."
redirect_to root_path and return false
end
return true
end
You could simplify it more (in my opinion) by splitting out the different parts of behavior (authorization, handling bad authorization) like this:
def destroy
#post = Post.find_by_slug(params[:slug])
punt("You cannot mess with another author's post") and return unless author_of(#post)
...
end
protected
def author_of(post)
post.user == current_user
end
def punt(message)
flash[:notice] = message
redirect_to root_path
end
Personally, I prefer to offload all of this routine work to a plugin. My personal favorite authorization plugin is Authorization. I've used it with great success for the last several years.
That would refactor your controller to use variations on:
permit "author of :post"
The simple answer is to change the message to something that fits both: "You cannot mess with another author's posts."
If you don't like the ugly* return in that last solution, you can use an around filter and conditionally yield only if the user is authorized.
around_filter :check_authorization, :only => [:destroy, :update]
private
def check_authorization
#post = Post.find_by_slug(params[:slug])
if #post.user == current_user
yield
else
flash[:notice] = case action_name
when "destroy"
"You cannot delete another author's posts."
when "update"
"You cannot edit another author's posts."
end
redirect_to root_path
end
end
*-- that's my preference, though code-wise it's perfectly valid. I just find that style-wise, it tends to not fit.
I also should add I haven't tested this and am not 100% certain it would work, though it should be easy enough to try.

Resources