I found a security risk in my application that I do not know how to fix. I just found this out while doing some checking on HTML and was playing with it.
So I have this code on a form tag
<%= form_tag("/spree/admin/workorders/#{#workorder.id}
/?workflow_id=#{workflow.id}
&name=workstep_name_{workflow.product_workstep.workstep.name}
&workstep_id=#{workflow.product_workstep.workstep.id}", method: "put")do%>
that query parameters will be caught by this method.
def update
#incoming_status = params[params[:name]]
# grab workflow, this is current data, use this to compare status to in comming status
#workflow = get_workorder_product_workstep(params[:workflow_id])
if is_workstep_for_shipping?
if #workorder.order.shipment_state.downcase == "shipped"
update_workflow
else
flash[:notice] = "Product not yet shipped"
end
else
update_workflow
end
redirect_to admin_workorder_path(#workorder)
end
def update_workflow
# check current status if its pending allow to update
if #workflow.send("can_#{#incoming_status}?")
# update status
#workflow.send(#incoming_status.to_s)
# updated attribute handled_by
#workflow.update_attributes(handled_by_id: #curr_user.id)
#workflow.save
else
flash[:notice] = 'Action not allowed'
end
end
and on the browser, the form that was created in view will render this.
<form action="/spree/admin/workorders/45/?workflow_id=141&name=workstep_name_w4&workstep_id=61" accept-charset="UTF-8" method="post">
</form>
now that form, those query parameters value can be changed. tried inputting wrong value and my backend crashed. how can fix this one? any idea or article that I could read? thank you.
UPDATE
While playing with the network. I also found this on network tab request payload
{spree_user: {email: "-----, password: "---"}}
spree_user: {email: "----", password: "----"}
everything is on plain text. I just changed my email and password to ----
If a person with a tech background they can easily spot this.
The code is from production. I think we are not using JWT here.
Related
I've just upgraded an app from Rails 3 to Rails 4, and I'm seeing a bunch of InvalidAuthenticityToken exceptions popping up. Digging in it looks like it is fairly common for our users to have multiple long-lived tabs open on our site. So I think what's happening is something like this: user Alice has three tabs open and her session expires. She logs back in on one of the tabs, which updates the authenticity token stored in her session. Then she returns to one of the other open tabs and tries to submit data, but she gets a 500 error from the InvalidAuthenticityToken error we raised.
It would clearly be nice to do some error handling for Alice so she doesn't get a 500 error. I'm wondering about best practices for this kind of situation. What would be a nice way to handle Alice's submission from the expired tab? I don't want to expire the current session, because that would be super annoying from the user's perspective ("I just logged in, you dolt!"). Ideally, I just want the user to reload the page, which would result in the correct authenticity token being present in the form. Or should I be doing something different so that the long-lived tabs that are open notice that the session has expired and force a reload? This would probably be sub-optimal from the user's point of view, because they liked having that page ready and easily accessible to keep referencing, which is why they left it open in the tab in the first place.
In my rails 4.1.1 app I had the same problem. I solved it by adding this code to ApplicationController. I found this solution here.
rescue_from ActionController::InvalidAuthenticityToken, with: :redirect_to_referer_or_path
def redirect_to_referer_or_path
flash[:notice] = "Please try again."
redirect_to request.referer
end
This way any controller that inherits from ApplicationController will handle the error with a redirect to the page the form was submitted from with a flash message to give the user some indication of what went wrong. Note this uses the hash syntax introduced in Ruby 1.9. For older versions of Ruby you will need to use :with => :redirect_to_referer_or_path
The solution to this problem can be divided into 2 phases. Phase 1 addresses the issue of ActionController::InvalidAuthenticityToken error and phase 2 deals with the issue of long tabs waiting idly.
Phase 1(1st variation)
One way to go about is redirect the user back to their location before the error. For ex. if Alice has 3 tabs open, the first one expires and Alice logs in again in it because she was browsing on it. But when she moves to tab 3 which has URL 'http://example.com/ex' and submits a form. Now instead of displaying her an error we can redirect her back to 'http://example.com/ex' with her submitted form values already pre-filled in the form for easy use.
This can be achieved by following this approach:
1) ApplicationController - Add this function:
def handle_unverified_request
flash[:error] = 'Kindly retry.' # show this error in your layout
referrer_url = URI.parse(request.referrer) rescue URI.parse(some_default_url)
# need to have a default in case referrer is not given
# append the query string to the referrer url
referrer_url.query = Rack::Utils.parse_nested_query('').
merge(params[params.keys[2]]). # this may be different for you
to_query
# redirect to the referrer url with the modified query string
redirect_to referrer_url.to_s
end
2) You need to include a default value for all your form fields. It will be the name of that field.
...
<% f.text_field, name: 'email', placeholder: 'Email', value: params[:email] %>
...
This way whenever Alice will submit a form with wrong authenticity_token she will be redirected back to her form with the original values she submitted and she will be shown a flash message that kindly retry your request.
Phase 1(2nd variation)
Another way to go about is just redirect Alice back to the form which she submitted without any pre-filled values.
This approach can be achieved by:
1) ApplicationController - Add this function:
def handle_unverified_request
flash[:error] = 'Kindly retry.'
redirect_to :back
end
Phase 2
To tackle the problem of long awaited tabs you can take the help of SSEs. Rails 4 has ActionController::Live for handling SSEs.
1) Add this to any controller:
include ActionController::Live
...
def sse
response.headers['Content-Type'] = 'text/event-stream'
sse = SSE.new(response.stream, retry: 2000, event: 'refresh') # change the time interval to your suiting
if user_signed_in? # check if user is signed-in or not
sse.write('none')
else
sse.write('refresh')
end
ensure
sse.close
end
2) Give the above function a GET route in your routes file. Lets call this route '/sse'
3) Add this in your layout:
<% if user_signed_in? %> # check if user is signed-in or not
<script>
var evtSource = new EventSource("/sse");
evtSource.addEventListener('refresh', function(e){
if(e.data == 'refresh'){
window.location = window.location.href;
}
});
</script>
<% end %>
Note: using EventSource is not supported by all browsers. Please check out the Browser compatibility section.
Source:
rails 4 redirect back with new params & MDN: Using server-sent events
Answer by Durrell is perfectly alright, I am just providing an alternative way to write the same thing. This needs to go in ApplicationController.
rescue_from ActionController::InvalidAuthenticityToken do |_exception|
flash[:alert] = 'Please try again.'
redirect_back fallback_location: root_path
end
Think about a simple Rails scaffold application with a "new" action containing a form to add records to a database with a "save" button. After the "create" action the controller redirects to the "show" action, where the user can use the "edit" link to edit the just inserted record. So far, so simple.
But if the user instead uses the browser's back button after creating a record to get back to the "new" action, the browser shows the form with the values the user just has entered. Now he changes some values and presses "save" again. He thinks that this would change the record, but of course this creates a new record.
What is the preferred way to prevent such duplicate entries? I'm looking for a general solution, maybe based on cookies or JavaScript.
After some investigations I found a suitable solution based on cookies. Here it is:
In the controller's "new" action, a timestamp with the current time is generated and rendered in the form as hidden field. When the user submits the form, this timestamps gets back to the controller's "create" action. After creating the record, this timestamp is stored in the session cookie. If the user goes back to the "new" form via browser's back button, he gets a stale form, which means its timestamp is older than the one stored in the cookie. This is checked before creating the record and results in an error message.
Here is the controller code:
def new
#post = Post.new
#stale_form_check_timestamp = Time.now.to_i
end
def create
#post = Post.new(params[:post])
if session[:last_created_at].to_i > params[:timestamp].to_i
flash[:error] = 'This form is stale!'
render 'new'
else
#post.save!
#stale_form_check_timestamp = Time.now.to_i
session[:last_created_at] = #stale_form_check_timestamp
end
end
And here the form code:
- form_for #post do |f|
= tag :input, :type => 'hidden', :name => 'timestamp', :value => #stale_form_check_timestamp
= f.input :some_field
= .......
When I had that same problem I created this little gem that solves it. When the user hits back, he's redirected to the edit_path of the record, instead of going back to the new_path.
https://github.com/yossi-shasho/redirect_on_back
You can do something like:
def create
#user = User.new(params[:user])
if result = #user.save
redirect_on_back_to edit_user_path(#user) # If user hits 'back' he'll be redirected to edit_user_path
redirect_to #user
end
end
Your model validations will ensure things like email addresses are unique, but I think this is more about usability and experience than anything else.
Say you are talking about an account creation form. First of all, your form submit button should say something like "Create Account", instead of just "Submit". Then depending on whether it was successful or not, show a message like either "Account successfully created" or "There were errors creating your account". If the user sees this message, they will know what happened.
Sure you can't prevent someone from hitting the back button and hitting enter again, but you should design for the majority of use cases. If they happen to hit back, they will see the button that says "Create Account". You should probably have some other text on the page that says "Please sign up for a new account to get started".
Just my $0.02.
Session or cookie may result in sides effects.
I totally agree : if there is a way to validate with your model, it's the safest way to prevent duplicate records.
Still you can do 2 things. Prevent browser caching : fields will appear empty in the form when the user clicks on the back button. And disable the "Create" button when clicked.
= f.submit "Create", :disable_with => "Processing..."
When your user will press the back button the button will be disabled.
You can use validators to make sure that no duplicate values are inserted. In this case validates_uniqueness_of :field
If you for example want to prevent users from having the same email address you could put the following code in your user model.
validates_uniqueness_of :email
This checks the column for any previous entries that are the same as the one your trying to inert.
Good luck
base on #Georg Ledermann answer i make this little snip of code for redirect to edit path if the user hits back and then hits create.
#objects_controller.rb
def new
#object = Object.new
#stale_form_check = Time.now.to_i
end
def create
#object = Object.new(object_params)
#function defined in application_controller.rb
redirect_to_on_back_and_create(#object)
end
#application_controller.rb
private
def redirect_to_on_back_and_create(object)
if session[:last_stale].present? and session[:last_stale_id].present? and session[:last_stale].to_i == params[:stale_form_check].to_i
redirect_to edit_polymorphic_path(object.class.find(session[:last_stale_id].to_i)), alert: "Este #{object.model_name.human} ya ha sido creado, puedes editarlo a continuación"
else
if object.save
session[:last_stale] = params[:stale_form_check].to_i
session[:last_stale_id] = object.id
redirect_to object, notice: "#{object.model_name.human} Creado con éxito"
else
render :new
end
end
end
And finally add the #stale_form_check param to your form
<%= hidden_field_tag :stale_form_check, #stale_form_check %>
You could always abstracts this method where you need it, but in this way you could avoid lots of repetition in your project if you need this behavior in many parts
Hope it helps the next one, i used to use redirect_on_back gem, but it didn't work for me this time, the _usec param that this gem uses, was always been reset, so it can't compare in every time when it was need
Here's something that worked for me.
You will need to do 2 things: Create a method in your controller and add a conditional statement in that same controller under your 'create' method.
1) Your method should return the total count of that object from that user.
EX:
def user
current_user.object.count
end
2) Add conditional statement in your 'create' method.
EXAMPLE:
def create
#object = Object.create(object_params)
#object.save if user == 0
redirect_to x_path
end
I hope this helps!
Add html: { autocomplete: "off" } in your form_for like this:
<%= form_for #object, url: xxx_path, html: { autocomplete: "off" } do |f| %>
My objective is to allow Admins the right to sign Users up for a Project.
Currently, Users can sign themselves up for Projects.
So I was thinking in order to allow Admin to do this.. do something like this :
haml
= link_to "Project Signup", card_signups_path + "?user=#{user.id}", :class => "button"
And pass the params[:user] so I can replace this controller with this :
if params[:user]
#card_signup = User.find(params[:user]).build_card_signup
else
#card_signup = current_user.build_card_signup
end
The trouble is though.. this is a 3 part signup process, and its loaded VIA AJAX, so I can't pass the ?user=#{user.id} in any of the steps after the first.. ( at least not by the same convention that I already did, or know how to )
What kind of strategy would you employ in this?
One possible way of accomplishing this, would be to add a hidden field to your form, that mirrors the parameter your passing in, if its found.
So if the parameter your passing in is user, in your view you want to add a hidden field something like:
<input type="hidden" name="user" and value="<%= params[:user] %>" />
or with a rails form helper:
hidden_field_tag 'user', params[:user]
This way the code in your controller can check for this parameter at each step on the receiving end and know who to save the object for. Something like this:
def create
#card_signup = CardSignup.new(params[:card_signup])
if params[:user] && params[:user].to_i > 0
##
##some logic here to make sure current_user is admin, as no one else is allowed to do this
##
#card_signup.user_id = params[:user]
else
#card_signup.user_id = current_user.id
end
##onto validating model and saving / redirecting / etc
end
But the ultimate goal here is to keep the user param around, whether its an initial GET parameter to the page, or a Put/Post from ajax/etc to submit the form, this parameter will be around.
One other security angle to check would also be in the 'new' action of this controller, and check that if the user param is present then the current_user is an administrator, otherwise redirect or display an error message. This combined with re-validating this on the create should provide a decent way of making sure no one else can make these requests. You could also put this in a before_filter and call it for only new and create to keep things clean.
I have a form where users can enter an isbn and it will try to lookup book data and save it.
When validation fails for the isbn lookup (for example if somebody entered it incorrectly), I would like it to redirect to another form where users can enter data in manually if the isbn lookup fails (but not if other validations like numerical price fail).
Any ideas on how to do this? Thanks for the help!
Trying to understand what you're trying to do: please correct me if my assumption is wrong.
If you can't save the model because the ISBN failed validation and you want to display a form for just the ISBN since the other fields are OK, there's a couple things you can do to hold the other attributes in the meantime:
Output them as hidden fields when you render the form
Store them in session so you can redirect
If you can't save the model then there doesn't seem to be any reason for redirecting to another action: the user is still trying to complete the create action, except you want to render a different form for just the ISBN.
Here's how I'd do it using session, so you can adapt this for redirecting to another action if you need to:
def create
book = Book.new( params[:book].reverse_merge(session[:unsaved_book]) )
if book.save?
session.delete[:unsaved_book]
flash[:notice] = 'I love it!'
redirect_to book
else
if book.errors.on[:isbn] && book.errors.length == 1
session[:unsaved_book] = params[:book]
flash[:error] = 'Sorry, wrong ISBN number.'
render 'unknown_isbn'
else
flash[:error] = 'Check your inputs.'
render 'new'
end
end
end
I'd give them the option to re enter the isbn if the lookup failed, as it might just be a typo.
For the redirecting part:
redirect_to invalid_input_path and return unless model.valid?
redirect_to isbn_lookup_failed_path and return unless model.do_isbn_lookup
....
I have 2 questions about authlogic:
1 - how to get cause of unsuccessful session creation (for example - not confirmed, blocked, wrong pass etc) to use it in app logic (route to another page for confimation, or re-enter pass etc)?
2 - question about integration formtastic and authlogic. How to show error messages on session creation , when login and pass is NOT provided. Everytime i submitting empty form - there are no any error message(but form.error_messages shows that you must enter login and pass), but if one of fields (login OR pass) provided - all works good.
1:
If you follow the suggested pattern for the login url:
def create
#user_session = UserSession.new(params[:user_session])
if #user_session.save
redirect_to your_url
else
render :action => 'new'
end
end
The #user_session var will be available to your new template. You can access any login errors with #user_session.error_messages. I believe that returns formatted HTML, not an array.
UPDATE
According to the docs, Authlogic errors behave exactly like active record, so, in order to map your controller logic, you would do something like
if #user_session.save
# Normal flow
else
if #user_session.errors.on(:password)
# do something
else
# do something else
end
end