I have a Rails 7 website that I want to restrict to only visitors from Switzerland. To do this, I check the user's country code in the Application Controller, and block access if the user is not from Switzerland. However, I also want to allow Googlebot to crawl the site for SEO purposes. To accomplish this, I've added a check in the block_unless_swiss method to allow access if the user agent contains the string 'Googlebot'. Is this the correct way to allow Googlebot to crawl a restricted website, or is there a better approach?
class ApplicationController < ActionController::Base
before_action :block_unless_swiss
def block_unless_swiss
return if Rails.env.development?
# Get the user's country code
country_code = request.location.country_code
user_agent = request.user_agent
# If the user is not from Switzerland is the Googlebot, render an error page
return if country_code == 'CH' || user_agent.include?('Googlebot')
render 'errors/403', status: :forbidden
end
end
Related
I'm trying to figure out a way to prevent someone from changing parts of the url. I've setup my website so that no one can sign up or create a account so anyone visiting is a guest.
For example, I've set the url in this part of the website to look for the value of 1a from /pathfinder/a/quest1/1a/q1sub1/ in order to display a certain part of the index page of q1sub1.
If someone were to change the value of 1a to 1n I would like to test if the url has changed and give some sort of error message.
Not sure what code excerpts to share in this case so let me know if you need more info.
I'm open to any ideas and appreciate any input. Thank you!
The, URL by design, can't be controlled by your application. But if you want to track where the user was previously, in all controller you have have access to request.referrer which should tell you where the request came from. You might add some conditional logic to your controller and redirect the user if the request.referrer is something you want to restrict. So in your controller action something like this:
def show # for example
if request.referrer != "http://example.com/myformpage"
redirect_to root_path, notice: "Invalid access"
else
# do stuff
end
end
However this is still not secure as headers can be spoofed (thanks to #Holgar Just). If you really want security in your application you should read the documentation and if you need more granular control over users permissions, maybe checkout CanCanCan gem
A way to do what you describe is to fingerprint the URLs before sending them to your client, using a hash function that includes a secret pepper. The idea is that if the client tampers with and URL the fingerprint won't match anymore, and since the fingerprint is generated with a server-side secret, the client won't be able to generate a new one that matches the new URL.
Here is an example.
The user will access /foo and pass a custom data parameter. The application will use this param to build a customized URL with format /my/secret/:one/path/:two, and redirect the client to it. This is just to keep things simple, and the same approach could be applied if the generated URL were to be used as a <a href="..."> in a page.
The generated URL contains a fingerprint, and if the client tampers with either the URL or the fingerprint, or if the fingerprint is missing, the application will respond with a 403.
Let's look at the code. The routes:
Rails.application.routes.draw do
get :foo, to: 'example#foo'
get '/my/secret/:one/path/:two', to: 'example#bar', as: 'bar'
end
And the controller:
class ExampleController < ApplicationController
# GET /foo?data=xx foo_path
#
def foo
user_data = request[:data]
go_to_path = bar_path(one: user_data, two: "foobar")
go_to_path += "?check=#{url_fingerprint(go_to_path)}"
redirect_to go_to_path
end
# GET /my/secret/:one/path/:two bar_path
#
def bar
unless valid_request?
render plain: "invalid request!", status: 403
return
end
render plain: "this is a good request", status: 200
end
private
SECRET = ENV.fetch("URL_FINGERPRINT_SECRET", "default secret")
# Calculate the fingerprint of a URL path to detect
# manual tampering.
#
# If you want to restrict this to a single client, add
# some unique identifier stored in a cookie.
#
def url_fingerprint(path)
Digest::SHA2.hexdigest(path + SECRET)
end
def valid_request?
return false unless params[:check].present?
params[:check] == url_fingerprint(request.path)
end
end
With this, the client would start with:
$ curl -i -s http://localhost:3000/foo?data=hello | grep Location
Location: http://localhost:3000/my/secret/hello/path/foobar?check=da343dd84accb4c0f5f7ff1d6e68152ac124ca1a39ce4746623bcb7b9043cab3
And then:
curl -i http://localhost:3000/my/secret/hello/path/foobar?check=da343dd84accb4c0f5f7ff1d6e68152ac124ca1a39ce4746623bcb7b9043cab3
HTTP/1.1 200 OK
this is a good request
But if the URL gets modified:
curl -i http://localhost:3000/my/secret/IWASCHANGED/path/foobar?check=da343dd84accb4c0f5f7ff1d6e68152ac124ca1a39ce4746623bcb7b9043cab3
HTTP/1.1 403 Forbidden
invalid request!
The same would happen if the fingerprint itself was modifed or if it was missing.
I am trying to allow access to the log-in/sign-up page for a admin user only from my computer or any other way that lets me only see the web page for an admin sing-up-log-in.
Or what do typical web applications do to restrict access to the public towards a certain web page? If there is a bets-practice way, I would like to implement that.
I currently have Devise installed.
You can use the authenticate_user! Devise helper, adding it as callback within the needed controller and specifying the methods you want to control.
For instance if you have a Post model, then adding the authenticate_user! in the PostController it'll ask the user to be logged to have access to the methods in that specific controller:
class PostsController < ApplicationController
before_action :authenticate_user!
If you want to restrict just some specific methods then you can play with only and/or except.
See: Devise will create some helpers to use inside your controllers
and views. To set up a controller with user authentication, just add
this before_action (assuming your devise model is 'User')
Devise - Controller filters and helpers
According to your comment then you can create a method in the ApplicationController in order to restrict all of your controllers and methods.
This way you can define an array of addresses, and if the remote_ip coming from the request is in the array then you give access, if isn't then perform any other action:
ApplicationController < ActionController::Base
before_action :protect
private
def protect
addresses = ['127.0.0.1', ...]
if addresses.include?(request.remote_ip)
# give access
else
# restrict access
end
end
end
But if you need something more sophisticated then you'd have to see on your Nginx or Apache, whatever you're using to deploy your project.
I normally restrict webpage access through controller methods. My recent use case was going to a webpage only when payment was successful but redirecting when it was not, if any body issued a get request for that page directly, it would result in 404.
In your case, there can be multiple option for setup.
You can use cookies to see users credentials using Action Dispatcher
Use Devise for users and then you can fix a certain role to a user through adding a new migration and assign roles yourself after registering or let them choose.
I will expect you followed Devise route. In the controller action check for current user's role.
If User Not signed in (using current_user == nil)
redirect to home page and then return
else
if
its admin you go ahead and use the success page as partial and let them see the page using `<%= render 'pages/mypage'%>` and use return to end
else
just redirect back to home page with a notice "Don't try this".
redirect_to root_path, notice: 'Don't try this' and then use
return to end
end
(Just for fun, to see how many times a user did this wrong action, you can also have a table which stores current_user and number_of_wrong_attempt, and store their email whenever they try to go that page without permission before redirect in controller. After that you can email them with a background rake task which checks for a certain false attempt threshold that: "Hey! Your registration is being removed because you are doing unprohibited actions")
How can I create a dynamic QR code on a rails app such that the moment it is scanned and successfully processed, the open page bearing the QR code can then just redirect to the success page.
This is similar to the whatsapp web implementation where the moment the android app scans the QR code, the page loads the messages.
Am more interested in is the management of the sessions. When the QR is scanned am able to reload the page where it was displayed and then redirect to another page. any idea?
You could update the User model to be able to store an unique token value to use in you QR Codes; e.g.
$ rails generate migration add_token_to_user token:string
Or a separate related model
$ rails generate model Token value:string user:belongs_to
Then generate unique Token value that can be used within an URL and encode it
into a QRCode
# Gemfile
gem "rqrcode"
# app/models/token.rb
require "securerandom"
class User < ActiveRecord::Base
def generate_token
begin
self.token = SecureRandom.urlsafe_base64 #=> "b4GOKm4pOYU_-BOXcrUGDg"
end while self.class.exists?(token: token)
end
def qr_code
RQRCode::QRCode.new(
Rails.application.routes.url_helpers.url_for(
controller: "session",
action: "create",
email: email,
token: token
)
)
end
end
Then display this QRCode somewhere in your application
# app/views/somewhere.html.erb
<%= #token.qr_code.as_html %>
Then wire up your application's routes and controllers to process that generated
and encoded QRCode URL
# config/routes.rb
Rails.application.routes.draw do
# ...
get "/login", to: "sessions#new"
end
# app/controller/sessions_controller.rb
class SessionsController < ApplicationController
def create
user = User.find_by(email: params[:email], token: params[:token])
if user
session[:user_id] = user.id # login user
user.update(token: nil) # nullify token, so it cannot be reused
redirect_to user
else
redirect_to root_path
end
end
end
References:
whomwah/rqrcode: A Ruby library that encodes QR Codes
Module: SecureRandom (Ruby 2_2_1)
#352 Securing an API - RailsCasts
I am adding a new answer for two reasons:
1. Acacia repharse the question with an emphasis on What's App redirection of
the page with the QR Code being view, which I did not address in my initial
solution due a misunderstanding of the problem, and
2. Some people have found the first answer helpful and this new answer would
change it significantly that whilst similar, but no longer the same
When the QR is scanned am able to reload the page where it was displayed and
then redirect to another page
-- Acacia
In order to achieve this there requires to some kind of open connection on the
page that is displaying the QRCode that something interpretting said QRCode can
use to effect it. However, because of the application you trying to mimic
requires that only that one User viewing the page is effected, whilst not
actually being logged in yet, would require something in the page to be unique.
For the solution to this problem you will need a couple of things:
An unique token to identify the not logged-in User can use to be contacted /
influenced by an external browser
A way of logging in using JavaScript, in order to update the viewed page to
be logged after previous step's event
Some kind of authentication Token that can be exchange between the
application and the external QRCode scanner application, in order to
authentication themselves as a specific User
The following solution stubs out the above 3rd step since this is to
demonstrate the idea and is primarily focused on the server-side of the
application. That being said, the solution to the 3rd step should be as simple
as passing the know User authentication token by appending it to the URL within
the QRCode as an additional paramater (and submitting it as a POST request,
rather than as a GET request in this demonstration).
You will need some random Tokens to use to authentication the User with and
exchange via URL embedded within the QCcode; e.g.
$ rails generate model Token type:string value:string user:belongs_to
type is a reserverd keyword within Rails, used for Single Table Inheritance.
It will be used to specific different kinds of / specialized Tokens within this
application.
To generate unique Token value that can be used within an URL and encode it
into a QRCode, use something like the following model(s) and code:
# Gemfile
gem "rqrcode" # QRCode generation
# app/models/token.rb
require "securerandom" # used for random token value generation
class Token < ApplicationRecord
before_create :generate_token_value
belongs_to :user
def generate_token_value
begin
self.value = SecureRandom.urlsafe_base64 #=> "b4GOKm4pOYU_-BOXcrUGDg"
end while self.class.exists?(value: value)
end
def qr_code(room_id)
RQRCode::QRCode.new(consume_url(room_id))
end
def consume_url(room_id)
Rails.application.routes.url_helpers.url_for(
host: "localhost:3000",
controller: "tokens",
action: "consume",
user_token: value,
room_id: room_id
)
end
end
# app/models/external_token.rb
class ExternalToken < Token; end
# app/models/internal_token.rb
class InternalToken < Token; end
InternalTokens will be only used within the application itself, and are
short-lived
ExternalTokens will be only used to interact with the application from
outside; like your purposed mobile QRCode scanner application; where the User
has either previously registered themselves or has logged in to allow for
this authentication token to be generated and stored within the external app
Then display this QRCode somewhere in your application
# e.g. app/views/tokens/show.html.erb
<%= #external_token.qr_code(#room_id).as_html.html_safe %>
I also hide the current #room_id within the <head> tags of the application
using the following:
# e.g. app/views/tokens/show.html.erb
<%= content_for :head, #room_id.html_safe %>
# app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title>QrcodeApp</title>
<!-- ... -->
<%= tag("meta", name: "room-id", content: content_for(:head)) %>
<!-- ... -->
</head>
<body>
<%= yield %>
</body>
</html>
Then wire up your application's routes and controllers to process that generated
and encoded QRCode URL.
For Routes we need:
Route to present the QRCode tokens; "token#show"
Route to consume / process the QRCode tokens; "token#consume"
Route to log the User in with, over AJAX; "sessions#create"
We will also need some way of opening a connection within the display Token page
that can be interacted with to force it to login, for that we will need:
mount ActionCable.server => "/cable"
This will require Rails 5 and ActionCable to implment, otherwise another
Pub/Sub solution; like Faye; will need to be used instead with older versions.
All together the routes look kind of like this:
# config/routes.rb
Rails.application.routes.draw do
# ...
# Serve websocket cable requests in-process
mount ActionCable.server => "/cable"
get "/token-login", to: "tokens#consume"
post "/login", to: "sessions#create"
get "/logout", to: "sessions#destroy"
get "welcome", to: "welcome#show"
root "tokens#show"
end
Then Controllers for those actions are as follows:
# app/controller/tokens_controller.rb
class TokensController < ApplicationController
def show
# Ignore this, its just randomly, grabbing an User for their Token. You
# would handle this in the mobile application the User is logged into
session[:user_id] = User.all.sample.id
#user = User.find(session[:user_id])
# #user_token = Token.create(type: "ExternalToken", user: #user)
#user_token = ExternalToken.create(user: #user)
# keep this line
#room_id = SecureRandom.urlsafe_base64
end
def consume
room_id = params[:room_id]
user_token = params[:user_token] # This will come from the Mobile App
if user_token && room_id
# user = Token.find_by(type: "ExternalToken", value: user_token).user
# password_token = Token.create(type: "InternalToken", user_id: user.id)
user = ExternalToken.find_by(value: user_token).user
password_token = InternalToken.create(user: user)
# The `user.password_token` is another random token that only the
# application knows about and will be re-submitted back to the application
# to confirm the login for that user in the open room session
ActionCable.server.broadcast("token_logins_#{room_id}",
user_email: user.email,
user_password_token: password_token.value)
head :ok
else
redirect_to "tokens#show"
end
end
end
The Tokens Controller show action primarily generates the #room_id value for
reuse in the view templates. The rest of the code in the show is just used to
demonstrate this kind of application.
The Tokens Controller consume action requires a room_id and user_token to
proceed, otherwise redirects the User back to QRCode sign in page. When they are
provided it then generates an InternalToken that is associated with the User
of the ExternalToken that it will then use to push a notification / event to
all rooms with said room_id (where there is only one that is unique to the
User viewing the QRCode page that generate this URL) whilst providing the
necessary authentication information for a User (or in this case our
application) to log into the application without a password, by quickly
generating an InternalToken to use instead.
You could also pass in the User e-mail as param if the external application
knows about it, rather than assuming its correct in this demonstration example.
For the Sessions Controller, as follows:
# app/controller/sessions_controller.rb
class SessionsController < ApplicationController
def create
user = User.find_by(email: params[:user_email])
internal_token = InternalToken.find_by(value: params[:user_password_token])
# Token.find_by(type: "InternalToken", value: params[:user_password_token])
if internal_token.user == user
session[:user_id] = user.id # login user
# nullify token, so it cannot be reused
internal_token.destroy
# reset User internal application password (maybe)
# user.update(password_token: SecureRandom.urlsafe_base64)
respond_to do |format|
format.json { render json: { success: true, url: welcome_url } }
format.html { redirect_to welcome_url }
end
else
redirect_to root_path
end
end
def destroy
session.delete(:user_id)
session[:user_id] = nil
#current_user = nil
redirect_to root_path
end
end
This Sessions Controller takes in the user_email and user_password_token to
make sure that these two match the same User internally before proceeding to
login. Then creates the user session with session[:user_id] and destroys the
internal_token, since it was a one time use only and is only used internally
within the application for this kind of authentication.
As well as, some kind of Welcome Controller for the Sessions create action to
redirect to after logging in
# app/controller/welcome_controller.rb
class WelcomeController < ApplicationController
def show
#user = current_user
redirect_to root_path unless current_user
end
private
def current_user
#current_user ||= User.find(session[:user_id])
end
end
Since this aplication uses
ActionCable, we
have already mounted the /cable path, now we need to setup a Channel that is
unique to a given User. However, since the User is not logged in yet, we use the
room_id value that was previously generated by the Tokens Controller show
action since its random and unique.
# app/channels/tokens_channel.rb
# Subscribe to `"tokens"` channel
class TokensChannel < ApplicationCable::Channel
def subscribed
stream_from "token_logins_#{params[:room_id]}" if params[:room_id]
end
end
That room_id was also embedded within the <head> (although it could a hidden
<div> element or the id attribtue of the QRCode, its up to you), which means
it can be pulled out to use in our JavaScript for receiving incoming boardcasts
to that room/QRCode; e.g.
// app/assets/javascripts/channels/tokens.js
var el = document.querySelectorAll('meta[name="room-id"]')[0];
var roomID = el.getAttribute('content');
App.tokens = App.cable.subscriptions.create(
{ channel: 'TokensChannel', room_id: roomID }, {
received: function(data) {
this.loginUser(data);
},
loginUser: function(data) {
var userEmail = data.user_email;
var userPasswordToken = data.user_password_token; // Mobile App's User token
var userData = {
user_email: userEmail,
user_password_token: userPasswordToken
};
// `csrf_meta_tags` value
var el = document.querySelectorAll('meta[name="csrf-token"]')[0];
var csrfToken = el.getAttribute('content');
var xmlhttp = new XMLHttpRequest();
// Handle POST response on `onreadystatechange` callback
xmlhttp.onreadystatechange = function() {
if (xmlhttp.readyState == XMLHttpRequest.DONE ) {
if (xmlhttp.status == 200) {
var response = JSON.parse(xmlhttp.response)
App.cable.subscriptions.remove({ channel: "TokensChannel",
room_id: roomID });
window.location.replace(response.url); // Redirect the current view
}
else if (xmlhttp.status == 400) {
alert('There was an error 400');
}
else {
alert('something else other than 200 was returned');
}
}
};
// Make User login POST request
xmlhttp.open(
"POST",
"<%= Rails.application.routes.url_helpers.url_for(
host: "localhost:3000", controller: "sessions", action: "create"
) %>",
true
);
// Add necessary headers (like `csrf_meta_tags`) before sending POST request
xmlhttp.setRequestHeader('X-CSRF-Token', csrfToken);
xmlhttp.setRequestHeader("Content-Type", "application/json");
xmlhttp.send(JSON.stringify(userData));
}
});
Really there is only two actions in this ActionCable subscription;
received required by ActionCable to handle incoming requests/events, and
loginUser our custom function
loginUser does the following:
Handles incoming data to build a new data object userData to POST back to
our application, which contains User information; user_email &
user_password_token; required to login over AJAX using an authentication
Token as the password (since its somewhat insecure, and passwords are usually
hashed; meaning that they unknown since they cannot be reversed)
Creates a new XMLHttpRequest() object to POST without jQuery, that sends a
POST request at the JSON login URL with the userData as login information,
whilst also appending the current HTML page CSRF token; e.g.
Otherwise the JSON request would fail without it
The xmlhttp.onreadystatechange callback function that is executed on a
response back from the xmlhttp.send(...) function call. It will unsubscribe
the User from the current room, since it is no longer needed, and redirect the
current page to the "Welcomw page" it received back in its response. Otherwise
it alerts the User something failed or went wrong
This will produce the following kind of application
You can access a copy of the project I worked on at the following URL:
Sonna/remote-url_qrcode-signin: ruby on rails - How can I implement Whatsapp life QR code authentication - Stack Overflow
The only this solution does not address is the rolling room token generation,
which would either require either a JavaScript library to generate/regenerate
the URL with the Room Token or a Controller Action that return a regenerated
QRCode as either image or HTML that can be immediately displayed within the
page. Either method still requires you to have some JavaScript that closes the
current connection and opens a new one with a new room/session token that can
used so that only it can receive mesages from, after a certain amount of time.
References:
Action Cable Overview — Ruby on Rails Guides
whomwah/rqrcode: A Ruby library that encodes QR Codes
Module: SecureRandom (Ruby 2_2_1)
#352 Securing an API - RailsCasts
I am new to rails, I have one model named "Topic", I want to get users those are currently viewing topic index page.
How do I get those users, can anybody help me?
Thanks in advance
Here is article which describe how to track active/online users use Redis. You can use this approach to track users who are currently viewing your page.
I would do this like so:
Similar to Jack Hardcastle's suggestion, you could have a column in the users table in your db which stores the path part of a user's url, and another column called "last_seen_at"
:string last_viewed_page
:datetime last_seen_at
Then, in your ApplicationController you could have a before_filter like this:
#in ApplicationController
before_action :set_last_seen_at_and_last_viewed_page
def set_last_seen_at_and_last_viewed_page
if current_user && request.method == :get
current_user.update_attributes(:last_seen_at => Time.now, :last_viewed_page => request.path)
end
end
The reason i test that request.method == :get is that this will only include pages that they actually load in the browser, and see the address in their address bar, and not form submission urls which are usually hidden from the user.
So, now you have the data, it's easy to query. We can never know who is on a page at this exact moment, since we don't know what they are actually doing - they might have gone to the toilet or closed their computer. We can only say that "this page is the last page these people looked at, and they did so within the last 15 minutes" or whatever.
So, if we wanted to use that criteria, of the last 15 minutes, we could say
#current_path = request.path
#users_on_current_path = User.where(["last_seen_at > ? and last_viewed_page = ?", 15.minutes.ago, #current_path])
With Ruby on Rails, my models are being created with increasing unique ids. For example, the first user has a user id of 1, the second 2, the third 3.
This is not good from a security perspective because if someone can snoop on the user id of the last created user (perhaps by creating a new user), they can infer your growth rate. They can also easily guess user ids.
Is there a good way to use random ids instead?
What have people done about this? Google search doesn't reveal much of anything.
I do not consider exposing user IDs to public as a security flaw, there should be other mechanisms for security. Maybe it is a "marketing security flaw" when visitors find out you do not have that million users they promise ;-)
Anyway:
To avoid IDs in urls at all you can use the user's login in all places. Make sure the login does not contain some special characters (./\#? etc.), that cause problems in routes (use a whitelist regex). Also login names may not be changed later, that can cause trouble if you have hard links/search engine entries to your pages.
Example calls are /users/Jeff and /users/Jeff/edit instead of /users/522047 and /users/522047/edit.
In your user class you need to override the to_param to use the login for routes instead of the user's id. This way there is no need to replace anything in your routes file nor in helpers like link_to #user.
class User < ActiveRecord::Base
def to_param
self.login
end
end
Then in every controller replace User.find by User.find_by_login:
class UsersController < ApplicationController
def show
#user = User.find_by_login(params[:id])
end
end
Or use a before_filter to replace the params before. For other controllers with nested resources use params[:user_id]:
class UsersController < ApplicationController
before_filter :get_id_from_login
def show
#user = User.find(params[:id])
end
private
# As users are not called by +id+ but by +login+ here is a function
# that converts a params[:id] containing an alphanumeric login to a
# params[:id] with a numeric id
def get_id_from_login
user = User.find_by_login(params[:id])
params[:id] = user.id unless user.nil?
end
end
Even if you would generate random INTEGER id it also can be compromted very easy. You should generate a random token for each user like MD5 or SHA1 ("asd342gdfg4534dfgdf"), then it would help you. And you should link to user profile with this random hash.
Note, this is not actually the hash concept, it just a random string.
Another way is to link to user with their nick, for example.
However, my guess is knowing the users ID or users count or users growth rate is not a vulnerability itself!
Add a field called random_id or whatever you want to your User model. Then when creating a user, place this code in your UsersController:
def create
...
user.random_id = User.generate_random_id
user.save
end
And place this code in your User class:
# random_id will contain capital letters and numbers only
def self.generate_random_id(size = 8)
alphanumerics = ('0'..'9').to_a + ('A'..'Z').to_a
key = (0..size).map {alphanumerics[Kernel.rand(36)]}.join
# if random_id exists in database, regenerate key
key = generate_random_id(size) if User.find_by_random_id(key)
# output the key
return key
end
If you need lowercase letters too, add them to alphanumerics and make sure you get the correct random number from the kernel, i.e. Kernel.rand(62).
Also be sure to modify your routes and other controllers to utilize the random_id instead of the default id.
You need to add a proper authorization layer to prevent un-authorized access.
Let us say you you display the user information in show action of the Users controller and the code is as shown below:
class UsersController < ActionController::Base
before_filter :require_user
def show
#user = User.find(params[:id])
end
end
This implementation is vulnerable to id guessing. You can easily fix it by ensuring that show action always shows the information of the logged in user:
def show
#user = current_user
end
Now regardless of what id is given in the URL you will display the current users profile.
Let us say that we want to allow account admin and account owner to access the show action:
def show
#user = current_user.has_role?(:admin) ? User.find(params[:id]) : current_user
end
OTH authorization logic is better implemented using a gem like CanCan.