security safe to disable csrf tokens for json rails calls? - ios

I have an existing rails backend website which makes json calls to server. Now,I am developing a mobile iOS app to use the same backend and send calls in json. However, mobile requests are failing with:
WARNING: Can't verify CSRF token authenticity
Searching around stackoverflow, many suggested to disable csrf checks for json calls by using something like this:
# Or this in your application_controller.rb
def verified_request?
if request.content_type == "application/json"
true
else
super()
end
end
But my question is , I dont understand how does this prevent csrf attacks in json format? Attacker can always send a json request to our endpoint from their site. Anyone has insights into this? I couldn't find any clear answer to this.

What you are describing is very easy to exploit using Flash:
var request:URLRequest = new URLRequest("http://stackoverflow.com");
request.requestHeaders.push(new URLRequestHeader('Content-Type', 'application/json'));
request.data = unescape('{"a":1,"b":{"c":3}}');
request.method = URLRequestMethod.POST;
navigateToURL(request, '_blank');
If you look at the CSRF prevention cheat sheet you can check the referer to make sure its from a domain you trust. If the referer is blank then it could be originating from a https url, so that should be considered a failure. Relying on Ruby's CSRF token is a stronger form a CSRF protection.

This is a fix for ajax
Get csrf_token from rails or if using something else, from meta
// js file
var csrf_token = $('meta[name=csrf-token]').attr('content');
or
//js.erb file
var csrf_token = "<%= request.session["<%= _csrf_token %>"] %>";
then add this to js
$("body").bind("ajaxSend", function(elm, xhr, s){
if (s.type == "POST") {
// place lines mentioned above here
// line goes here...
xhr.setRequestHeader('X-CSRF-Token', csrf_token);
}
});

One approach you can take is to leave on CSRF checks but disable based on a http header being present. If your json requests have a JWT token, you can do something like this in the relevant controllers.
protect_from_forgery with: :exception, unless: -> { some_auth_token.valid? }
Form submissions won't be able to set a http header and thus csrf will protect against it.
You json requests should have an Authorization header to be secure.

Related

How to get CSRF tokens in a React/Rails App? ActionController::InvalidAuthenticityToken

Before going into more detail about the title, I just want to describe the problem at a basic level. I'm getting the dreaded 422, Unprocessable Entity error (ActionController::InvalidAuthenticityToken) after asynchronous requests in my app. What's the best way to handle this?
Loads of answers will say do:
skip_before_action :verify_authenticity_token
(which is now skip_forgery_protection in Rails 6), and then usually in the comments people get way more upvotes asking about whether or not that's a security risk. There are probably 5 threads like that.
The alternatives to doing this though, is sending the csrf token along with the POST request. So, most answers say make sure to include these headers
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
and also a csrf token like:
'X-CSRF-Token': csrfToken
The issue is getting the csrf token itself. I've tried displaying the contents of the header object with
const getHeaders = () => {
let headers = new window.Headers({
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
})
const csrfToken = document.head.querySelector("[name='csrf-token']")
if (csrfToken) { headers.append('X-CSRF-Token', csrfToken) }
return headers
}
and
let headers = getHeaders();
headers.forEach(function(value, name) {
console.log(name + ": " + value);
});
and that doesn't work. I'm guessing however I'm actually getting the token isn't working. Namely:
document.head.querySelector("[name='csrf-token']")
What's the best way to do this with plain ol' JavaScript, no other libraries? I've tried other suggestions and methods to attempt to get the token but so far all have failed. I'm assuming this lives somewhere on my browser but where? I can see all the data in Redux dev tools so the issue is definitely just this token as opposed to sending the correct data correctly (not to mention skip_forgery_protection completely solves it).
In the mean time sadly skip_forgery_protection works perfectly fine added to my Rails controllers, as security isn't the biggest concern in this stage, but I would rather fix the token issue. Thanks!

Is it dangerous to get CSRF token from response header?

If we use template engine at server side, we can pass CSRF token to client like this:
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
(From this Article)
But if we can't use template engine at all, we should pass CSRF token to client using response header like this:
CsrfToken token = (CsrfToken) request.getAttribute("_csrf");
// Spring Security will allow the Token to be included in this header name
response.setHeader("X-CSRF-HEADER", token.getHeaderName());
// Spring Security will allow the token to be included in this parameter name
response.setHeader("X-CSRF-PARAM", token.getParameterName());
// this is the value of the token to be included as either a header or an HTTP parameter
response.setHeader("X-CSRF-TOKEN", token.getToken());
public static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = HttpSessionCsrfTokenRepository.class.getName().concat(".CSRF_TOKEN");
CsrfToken sessionToken = (CsrfToken) request.getSession().getAttribute(DEFAULT_CSRF_TOKEN_ATTR_NAME);
(From this Article)
In the latter way, server should provide dummy REST API to client because client can't do AJAX call before first HTML page is loaded.
Is it dangerous to get CSRF token like latter way?

Error in Access-Control-Allow-Origin when Angular request to Rails

I'm using Angular 2 to make an API(rails) request. When I make the http request through angular, I get the following error:
XMLHttpRequest cannot load https://api-url. Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:4200' is therefore not allowed access. The response had HTTP status code 404.
However, if I try to make a request through Postman or the browser itself, I don't get any error. The data is displayed normally
angular code:
makeRequest() {
let user = {"user": "user", "password": "password"};
let headers: Headers = new Headers();
headers.append('Authorization', 'Basic ' + btoa(user.user + ':'+user.password));
headers.append('Content-Type', 'application/vn.api+json')
let this.http.get(api-url, {headers: headers}).map(res => res.json()).subscribe(data => {
this.data = data;
})
}
In my rails server i using the gem 'jsonapi-resources'to open AP. In my api controller, i have this code to authenticate and set headers to requests:
module Api
class ApiController < JSONAPI::ResourceController
prepend_before_action :set_headers, :authenticate
def context
{ current_station: #user }
end
private
def authenticate
authenticate_or_request_with_http_basic do |token, _|
#user = User.where(api_key: token).first
end
end
def set_headers
response.headers["Access-Control-Allow-Origin"] = "*"
end
end
end
When i make request in browser or using postaman, the header appears normally, but in Angular i have the error.
This is because of CORS. Long story short, browsers forbid by default one domain (http://localhost:4200) to make AJAX requests to another one (http://api-url). It works in "postman" because this is an extension and then, the CORS does not apply. If you want to fix this issue, you have to configure your server to returns certain header saying to the client that it allows the CORS connection.
In fact, when a website is trying to make an AJAX request to another domain, it first send an OPTION request to ask what are the domain allowed. This list is returned by the server via the header Access-Control-Allow-Origin. For example, it could contain a star ("*") to indicate that anyone could make AJAX call to this server. If this header allows your client to make AJAX call, your actual request will be executed, otherwise, you'll get an error (probably the one you currently get)

Authenticating to an API with a token

I'm working with the Zendesk API, an HTTPS-only, JSON API and authentication is required to update a resource, and the API supplies an API token to use when authenticating as different users. When updating a resource, I issue a PUT request over SSL, assign the appropriate JSON content to the request body, and specify the Content-Type request header as application/json.
Next, the API instructs its users to authenticate as the end-user by either using the user's email and password (which I can't do for several reasons) or to use the user's email address along with the API token. The following is my attempt to authorize to the API with the Authorization header:
#id = params[:id]
#comment_body = params[:comment]
uri = URI.parse "https://{subdomain}.zendesk.com/api/v2/requests/#{#id}.json"
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
req = Net::HTTP::Put.new(uri.request_uri)
req.body = '{"request": {"comment":{"value":' + "\"#{#comment_body}\"" + '}}}'
req['Content-Type'] = 'application/json'
#The following two lines do not work!
credentials = Base64::encode64("{user_email}/token:{api_token}")
request.headers['Authorization'] = "Basic #{credentials}"
response = http.request(req)
The API specifies that the format for authentication using the API token is {user_email}/token:{api_token}. I encoded that format with Base64::encode64 and passed it to the Authorization Header preceded with Basic, but the response is a 401 Unauthorized. However, replacing those two lines with req.basic_auth {user_email}, {user_password} works fine.
So my question is, how can I authenticate as a different user using the email and the given API token as authentication instead of supplying the user's email and password to req.basic_auth?
The googling I've done on the topic has revealed very little; apparently it's a lot more common to use the normal {username}:{password} format when dealing with the Authorization header than an API token.
Thanks in advance!!
Update: Weirdly, trying to authenticate as the end-user with req['Authorization'] = "Basic #{credentials}" does not return a 401 Unauthorized Error or a WWW-Authenticate header while trying to authorize as request.headers['Authorize'] = "Basic #{credentials}" does.
Finally figured it out after much head-banging and nearly throwing my laptop out the window. Suddenly, the answer seems incredibly obvious.
When using Net::HTTP, its basic_auth method can also accept tokens depending on the API, and the Zendesk API specifies that the format for using the API token is {email}/token:{token}. Basic authentication uses the format {username}:{password}, where the two fields are separated by a colon, meaning in Zendesk's case I can place {email}/token as the first argument and {token} as the second argument (instead of the username as the first argument and the password as the second argument), so the following code is correct:
req.basic_auth "{email}/token", "{api_token}"
I hope anyone who found this useful could leave a comment. Nice to know I spared someone from this frustration.

Retrieving the url anchor in a werkzeug request

I have a DAV protocol that stores out-of-band data in the url anchor, e.g. the ghi in DELETE /abc.def#ghi. The server is a Flask application.
I can see the request come in on the wire via tcpdump, but when I look at the werkzeug Request object (such as url() or base_url()), all I get back is /abc.def. The #ghi has been stripped out.
Is there a method that returns this information, or do I have to subclass Request to handle this myself? If so, is there an example I can use as an inspiration?
I ran into the same problem. Facebook authentication API returns the access token behind a hash appended into the redirection url. In the same way, Flask's request.url drops everything in the URL behind the hash sign.
I'm also using Flask so I think you can use my brute-force workaround using Javascript's window.location.href to get the full URL. Then, I just extracted the piece that I needed (the access token), put it into a redirection URL where I can pass the access token as an argument to the receiving view function. Here's the code:
#app.route('/app_response/<response>', methods=['GET'])
def app_response_code(response):
return ''' <script type="text/javascript">
var token = window.location.href.split("access_token=")[1];
window.location = "/app_response_token/" + token;
</script> '''
#app.route('/app_response_token/<token>/', methods=['GET'])
def app_response_token(token):
return token
In case you manage(d) to do this within Werkzeug, I'm interested to know how.
From Wikipedia (Fragment Identifier) (don't have the time to find it in the RFC):
The fragment identifier functions differently than the rest of the URI: namely, its processing is exclusively client-side with no participation from the server
So Flask - or any other framework - doesn't have access to #ghi.
You can do this using flask.url_for with the _anchor keyword argument:
url_for('abc.def', _anchor='ghi')

Resources