Secure Ajax from Non-Secure Pages

Clinton R. Dreisbach, Former Viget

Article Category: #Code

Posted on

Ok, here’s a problem: you have a login form on your web app that needs to be protected via SSL. In addition, on every other page – none of which use SSL – you have a miniature login form that submits via Ajax. Of course, the JavaScript same origin policy prevents you from making an Ajax request from your non-secure page to a secure page, even though they’re on the same site. You can’t just make the Ajax login use HTTP, because then you’d be sending passwords in the clear. So what do you do?

When David and I had to tackle this last week, we used a combination of <script> tag insertion and cryptography to rock this issue. We decided to make the Ajax request use HTTP, but encrypt the username and password. We thought of a few hashing schemes at first, but we quickly realized none were secure. We were going to have to have a shared secret that our JavaScript used to encrypt the parameters for the request and our Rails app would use to decrypt them. Then we were stuck with this problem: how do you have a shared secret between them that no one else knows?

First, we used SSL to transmit an encryption key and a reference to that key. We create the key in Ruby, store it in the database, and then transmit it with JSONP over SSL.

 class CryptoKeysController < ApplicationController ssl_required :show def show render :text => "#{params[:callback]}(#{CryptoKey.create.to_json});" end end 

In Javascript, we used two helper functions:

 var setCryptData = function (data) { window.cryptData = data.crypto_key; }; var submitFormWithCrypto = function (form, callback) { $.getScript('/profiles/crypto_key?callback=setCryptData', function () { var queryData = { key_id: cryptData.id, encrypted: teaEncrypt($(form).serialize(), cryptData.key) }; $.post(form.action.replace(/^https?:/, window.location.protocol), $.param(queryData), callback, "json"); }); }; 

In setCryptData, we set cryptData to be a global variable so we can access it from anywhere. In submitFormWithCrypto, we use jQuery’s getScript to get the key by inserting a script tag. This way, we can use SSL to prevent the key from being sniffed.

We pass a form DOM object and a callback function to submitFormWithCrypto so that the user can specify what to do with the return information from the Ajax request. submitFormWithCrypto grabs a new crypto key via SSL, then serializes the form and encrypts the query string created from the serialization with the Tiny Encryption Algorithm. You could use another algorithm here, of course. We were just looking for something fairly secure that was pretty fast to encrypt and decrypt. Once the form is serialized, we pass it and the id for the crypto key with our standard Ajax request for submitting the form. This was important – we didn’t want to write a lot of new code on the server handling the encrypted request as a special case.

On the server, we have this in our ApplicationController:

 class ApplicationController < ActionController::Base before_filter :decrypt_params def ssl_required? use_ssl? && (request.ssl? || !request.xhr?) && super end def decrypt_params if params[:key_id] && params[:encrypted] key = CryptoKey.search_and_destroy( params.delete(:key_id)) if key.nil? raise UnauthorizedException.new else query_string = Crypt::BlockTea.decrypt( params.delete(:encrypted), key) unencrypted_params = \ Rack::Utils.parse_nested_query(query_string) params.merge!(unencrypted_params) end end end end 

ssl_required? checks to make sure the request is either SSL-protected or an Ajax request if SSL is required. decrypt_params looks for the existence of a crypto key ID and encrypted parameters, and decrypts those and merges them into the normal parameters.

In under 100 lines of code, we have a secure method of passing parameters over HTTP, while using SSL to protect our shared encryption keys. Figuring out this sort of solution is why I became a programmer, and it was a blast to pair on it with a co-worker. You can look at all the code, including the Javascript and Ruby encryption/decryption algorithms in this Gist on GitHub.

Related Articles