Friday, March 14, 2008

OpenSocial and Security - here is your source code for everyone to explore

Why is MySpace so anal about reviewing apps and suspending them for things that are allowed on other application platforms?

Because OpenSocial is insecure. Yes, that's right, this javascript client-side technology exposes a lot of things. Want to see the ENTIRE source code of "Honesty Box" application? Easy! Install that app on MySpace and "view source" in your browser. 

Just to make it easy for you, I'll paste it here:




<script type="text/javascript">

String.prototype.to_rfc3986 = function (){
var tmp = encodeURIComponent(this);
tmp = tmp.replace('!','%21');
tmp = tmp.replace('*','%2A');
tmp = tmp.replace('(','%28');
tmp = tmp.replace(')','%29');
tmp = tmp.replace("'",'%27');
return tmp;
}


var ajaxServer = "www.honestybox.com";
var views = new Array("inbox","outbox","message","write","messagesent","invite","settings");
var tabs = new Array("tab_inbox","tab_outbox","tab_settings","tab_write");
var ownerId = null;
var viewerId = null;
var viewerGender = null;
var viewerFriends = null;
var debugStatus = false;
var msgid = 0;
var msie = false;
var classAttributeName = "class";
var friendDictionary = {};
var friendIdList = new Array();

// for caching user friend information
function User(userId, userName, userThumnail, userProfileURL,userGender){
this.UserId = userId;
this.UserName = userName;
this.UserThumbnail = userThumnail;
this.UserProfileURL = userProfileURL;
this.UserGender = userGender;
}

function loadUserData(){
var opt_params = {};
opt_params[opensocial.DataRequest.PeopleRequestFields.PROFILE_DETAILS] = opensocial.Person.Field.GENDER;

var dataRequest = opensocial.newDataRequest();
var ownerRequest = dataRequest.newFetchPersonRequest(opensocial.DataRequest.PersonId.OWNER);
var viewerRequest = dataRequest.newFetchPersonRequest(opensocial.DataRequest.PersonId.VIEWER, opt_params);
var viewerFriends = dataRequest.newFetchPeopleRequest(opensocial.DataRequest.Group.VIEWER_FRIENDS, opt_params);


// Check that viewer has app installed and hit with promo text / partner text
dataRequest.add(ownerRequest);
dataRequest.add(viewerRequest);
dataRequest.add(viewerFriends);
dataRequest.send(loadUserData_Callback);
}

function loadUserData_Callback(dataResponse){
if(!dataResponse.hadError()){
var ownerData = dataResponse.get(opensocial.DataRequest.PersonId.OWNER).getData();
ownerId = ownerData.getField(opensocial.Person.Field.ID);
var viewer = dataResponse.get(opensocial.DataRequest.PersonId.VIEWER);
if(viewer != null){
var viewerData = dataResponse.get(opensocial.DataRequest.PersonId.VIEWER).getData();
for(k in viewerData['fields_']){
renderStatus('Viewer: ' + k + ' = ' + viewerData['fields_'][k]);
}
viewerId = viewerData.getField(opensocial.Person.Field.ID);
viewerGender = viewerData.getField(opensocial.Person.Field.GENDER);
} else {
// user shouldn't be here!
}

var currentUser = new User(viewerData.getField(opensocial.Person.Field.ID), viewerData.getField(opensocial.Person.Field.NAME), viewerData.getField(opensocial.Person.Field. THUMBNAIL_URL), viewerData.getField(opensocial.Person.Field.PROFILE_URL), viewerData.getField(opensocial.Person.Field.GENDER));

var friendsData = dataResponse.get(opensocial.DataRequest.Group.VIEWER_FRIENDS).getData();

friendsData.each(function(friendData){
var friendName = friendData.getField(opensocial.Person.Field.NAME);
var friendThumbnailUrl = friendData.getField(opensocial.Person.Field.THUMBNAIL_URL);
var friendId = friendData.getField(opensocial.Person.Field.ID);
var friendURL = friendData.getField(opensocial.Person.Field.PROFILE_URL);
var friendGender = friendData.getField(opensocial.Person.Field.GENDER);

var friendObj = new User(friendId, friendName, friendThumbnailUrl, friendURL, friendGender);
friendDictionary[friendId] = friendObj;
friendIdList.push(friendId);
});

//Let's shove the current user into the "friendDictionary" so we can look him/her up without silly if login

friendDictionary[viewerId] = currentUser;

unrender('loading');
if(viewerId == ownerId){
render("navigation");
render("inbox");
} else {
render("navigation");
render("write");
}
} else {
unrender('loading');
renderError('Unable to determine who you are, if you have the application installed, refresh the page.');
}
}

function surface(type){
var surface_type = new opensocial.Surface(type);
opensocial.requestNavigateTo(surface_type);
}

function render(view){
// just turn on these items, without adjusting others
if(view == "status"
|| view == "error"
|| view == "navigation"
){
document.getElementById(view).setAttribute(classAttributeName,"visible");
return;
}

// make adjustments to other views
for(var i=0; i<views.length; i++){
if(view == views[i]){
document.getElementById(views[i]).setAttribute(classAttributeName,"visible");
} else {
document.getElementById(views[i]).setAttribute(classAttributeName,"invisible");
}
}

// special case views
if(view == "write") {init_write(); setActiveTab('tab_write'); }
if(view == "inbox"){ init_inbox(); setActiveTab('tab_inbox'); }
if(view == "outbox") { init_outbox(); setActiveTab('tab_outbox'); }
if(view == "message") { init_message(); setActiveTab('tab_inbox'); }
if(view == "settings") { init_settings(); setActiveTab('tab_settings'); }
if(view == "invite") { init_settings(); setActiveTab('tab_invite'); }
}

function unrender(view){
document.getElementById(view).setAttribute(classAttributeName,"invisible");
}

function setActiveTab(tab){
renderStatus('Changing to tab: ' + tab);
for(var i=0; i<tabs.length; i++){
if(tabs[i] == tab){
renderStatus('Turning ' + tab + ' on.');
document.getElementById(tabs[i]).setAttribute(classAttributeName,"active");
} else {
document.getElementById(tabs[i]).setAttribute(classAttributeName,"inactive");
}
}
renderStatus('Done changing tabs!');
}

function renderStatus(msg){
if(debugStatus){
render("status");
var el = document.getElementById("status");
el.innerHTML = "<b>Status:</b> " + msg + "<br />" + el.innerHTML;
}
}

function renderError(msg){
renderStatus('renderError invoked');
render("error");
var el = document.getElementById("error");
el.innerHTML += "<b>Error:</b> " + msg + "<br />";
}

function init_inbox(){
renderStatus('init_inbox invoked');
var params = {};
ajaxRequest("inbox",init_inbox_callback,params);
}

function init_inbox_callback(data){
renderStatus('init_inbox_callback invoked');
var msgs = "";
var i=0;
for(i=0; i<data.messages.length;i++){
msgs += format_thread_message(data.messages[i], true, data.messages[i].gender, 750);
}
var el = document.getElementById("inbox_messages");
el.innerHTML = msgs;
// gadgets.window.adjustHeight();
}

function init_outbox(){
renderStatus('init_outbox invoked');
ajaxRequest("outbox",init_outbox_callback, {});
}

function init_outbox_callback(data){
renderStatus('init_outbox_callback invoked');
var msgs = "";
var i=0;
for(i=0; i<data.messages.length;i++){
msgs += format_thread_message(data.messages[i], true, data.messages[i].gender, 750, friendDictionary[data.messages[i].recipient].UserThumbnail);
}
var el = document.getElementById("outbox_messages");
el.innerHTML = msgs;
// gadgets.window.adjustHeight();
}

function read_message(id){
renderStatus("read_message invoked - reading message " + id);
msgid = id;
render("message");
}

function init_message(){
renderStatus("init_message invoked");
if(msgid > 0){
renderStatus('loading message ' + msgid);
// Reset message canvas
var el = document.getElementById('message_history');
el.innerHTML = "";
var params = {};
params.msgid = msgid;
ajaxRequest("thread",init_message_callback, params);
} else {
renderError("Invalid message");
}
}

function init_message_callback(data){
renderStatus("init_message_callback invoked");
var msgs = "";
var i=0;
var icon = null;
for(i=0; i<data.messages.length;i++){
msgs += format_thread_message(data.messages[i], false, data.messages[i].gender, 750, null);
}
var el = document.getElementById("message_history");
el.innerHTML = msgs;
var frm = document.getElementById('reply_message_form');
// gadgets.window.adjustHeight();
frm.message.focus();
}

function ajaxRequest(method, callback_func, params){
// add some default values to the request
renderStatus('Preparing the request for owner ' + ownerId);
// params['surface'] = opensocial.Surface.getName();
var queryString = "method=" + method;
for (k in params) {
// queryString += "&" + k + "=" + encodeURIComponent(params[k]);
queryString += "&" + k + "=" + params[k].to_rfc3986();
}

// set the datasource location
var url = "http://" + ajaxServer + "/opensocial/ms_ajax.php?" + queryString + "&r=" + Math.random();

// set the opensocial params to sign the request and fetch JSON object
renderStatus("Setting opensocial call parameters");
var osParams = {};

osParams[gadgets.io.RequestParameters.AUTHORIZATION] = gadgets.io.AuthorizationType.SIGNED;
renderStatus("Making call to " + url);

gadgets.io.makeRequest(url, makeRequest_callback, osParams);

function makeRequest_callback(data){
renderStatus("Handling ajax response with typeof: " + typeof(data.data));
var json = gadgets.json.parse(data.data);
if(!json){
renderError("Error talking to server: " + json.ErrorMessage);
}
callback_func(json);
}
}


function init(){
renderStatus('Init invoked');
if(navigator.appName=="Microsoft Internet Explorer"){
msie = true;
classAttributeName = "className";
renderStatus('User agent appears to be IE');
}
render("loading");
loadUserData();
}

init();

function format_message(msg, sex){
renderStatus('format_message invoked');
var linked = true;
var strout = "";
strout += "<div class=\"message\">";
strout += '<a href="#no_anchor" onclick="read_message(\'' + msg.msgid + '\');">';
strout += msg.comment;
strout += "</a>";
strout += "</div>";
return strout;
}

function format_thread_message(msg, link , sex, width, icon){
renderStatus('format_thread_message invoked');
var columnWidth = width - 100;
var imgPrefix = "gray_";
var bgColor = "dddddd";
switch(sex){
case 'male':
imgPrefix = "blue_";
bgColor = "c1e6f6";
break;
case 'female':
imgPrefix = "pink_";
bgColor = "f9d9e6";
break;
}

if(icon != "undefined" && icon != null && icon.substr(0,4) == "http"){
var img = icon;
} else {
var img = 'http://img.honestybox.com/logo/hb_circle_logo_' + imgPrefix + '50x50.png';
}

var strout = "";
strout += '<div class=\"message\" style="width:' + width + 'px;">';
strout += '<img src="' + img + '" alt="" border="0" style="float:left; padding-right:10px;" />';
strout += '<div style="float:right;">';
strout += '<table border="0" cellpadding="0" cellspacing="0" width="' + columnWidth + '">';
strout += '<tr>';
strout += '<td><img src="http://img.honestybox.com/bubbles/' + imgPrefix + 'top_left.png" width="8" height="8" border="0" /></td>';
strout += '<td bgcolor="' + bgColor + '"><img src="http://img.honestybox.com/images/clear.gif" width="' + (columnWidth - 16) + '" height="8" border="0" /></td>';
strout += '<td align="right"><img src="http://img.honestybox.com/bubbles/' + imgPrefix + 'top_right.png" /></td>';
strout += '</tr><table border="0" cellpadding="0" cellspacing="0" width="' + columnWidth + '"><tr>';
strout += '<td colspan="3" bgcolor="' + bgColor + '"><div style="padding:0px 8px;">';
if(viewerId == msg.sender){
strout += "<b>You said:</b> ";
}
if(link){
strout += '<a href="#no_anchor" onclick="read_message(\'' + msg.msgid + '\');">';
}
if(typeof(msg) == "object"){
strout += msg.comment;
} else {
strout += msg;
}
if(link){
strout += '</a>';
}
strout += '</div></td>';
strout += '</tr><table border="0" cellpadding="0" cellspacing="0" width="' + columnWidth + '"><tr>';
strout += '<td valign="top"><img src="http://img.honestybox.com/bubbles/' + imgPrefix + 'bottom_left.png" border="0" /></td>';
strout += '<td valign="top"><img src="http://img.honestybox.com/bubbles/' + imgPrefix + 'bottom_gradient.png" width="' + (columnWidth - 48) + '" height="9" border="0" /></td>';
strout += '<td align="right" valign="top"><img src="http://img.honestybox.com/bubbles/' + imgPrefix + 'bottom_right.png" height="9" border="0" /></td>';
strout += '</tr>';
strout += '</table>';
strout += '</div>';
strout += '<br clear="all" />';
strout += "</div>";
return strout;
}


function init_write(){
renderStatus('Loading friend list');
var friend = null;
var id = null;
var el = document.getElementById("write_friends");
var strout = "";
for(id in friendDictionary){
friend = friendDictionary[id];
var comment_body = "Tell <b>" + friend.UserName + "</b> what you really think...<br />" + '<textarea name="message_' + friend.UserId + '" class="message" style="padding-bottom:5px; height:60px;"></textarea>'
strout += '<div id="f' + id + '">';
strout += format_thread_message(comment_body, false , "unknown", 750, friend.UserThumbnail);
strout += '</div>';
}
el.innerHTML = strout;
}

function send_messages(fe){
render("loading");
renderStatus("send_messagess invoked");
fe.form.submit_a.disabled = true;
fe.form.submit_b.disabled = true;
var params = {}
for(var i=0; i<fe.form.elements.length; i++){
if(fe.form.elements[i].name.substr(0,8) == "message_"){
params[fe.form.elements[i].name] = fe.form.elements[i].value;
fe.form.elements[i].value = "";
}
}
ajaxRequest('message_new_multi', send_messages_callback, params);
}

function send_messages_callback(json){
renderStatus("send_messages_callback invoked.");
var frm = document.getElementById("new_message_form");
frm.submit_a.disabled = false;
frm.submit_b.disabled = false;
render("messagesent");
}

function reply_message(fe){
render("loading");
renderStatus("reply_message invoked");
fe.disabled = true;
fe.form.message.disabled = true;
var params = {}
params['threadid'] = msgid;
params['comment'] = fe.form.message.value;
ajaxRequest('message_reply',reply_message_callback,params);
}

function reply_message_callback(data){
renderStatus("reply_message_callback invoked.");
render("message");
var frm = document.getElementById('reply_message_form');
frm.message.disabled = false;
frm.message.value = "";
frm.button_send.disabled = false;
renderStatus("Form reset.");
read_message(msgid);
}


function init_settings(){
renderStatus("init_settings invoked");
ajaxRequest('settings',init_settings_callback,{});
}

function init_settings_callback(data){
var i = 0;
unrender('loading');
renderStatus("init_settings_callback invoked.");
var frm = document.getElementById('settings_form');
frm.question.value = data.settings.status;
// set the gender from the data
for(i=0; i<frm.gender.options.length; i++){
if(frm.gender.options[i].value == data.settings.gender){
frm.gender.selectedIndex = i;
}
}
}

function settings_save(fe){
renderStatus('settings_save invoked');
var params = {};
params['question'] = fe.form.question.value;
params['gender'] = fe.form.gender.options[fe.form.gender.selectedIndex].value;
ajaxRequest('settings_save',settings_save_callback, params);
}

function settings_save_callback(){
renderStatus('settings_save_callback invoked');
render('settings');
}

</script>



You think that's bad? It gets even worse. How about the ENTIRE server-side code for one of the top Facebook apps? Yes, SERVER-SIDE, entire PHP code, database schema and everything else. You don't have to be a hacker to get to it - those guys left a few holes open and OpenSocial exposed them all.

Don't believe me? You can get it yourself if you like complete with all passwords and security keys, but here is an excerpt of their PHP code:



<?php
require 'config/myspace.php';
require 'smarty.php';
require 'database.php';
require_once 'classes/Notable.php';
require_once 'classes/User.php';
require_once 'classes/People.php';
require_once '../platform/Space.php';

$_GET['criteria'] = in_array($_GET['criteria'], array('from', 'to', 'top')) ? $_GET['criteria'] : 'top';

$current_user = User::get_current_user();
$friends_notables = Notable::get_cached_data(array('Notable', 'list_friends_superlatives'), array($_GET['criteria'], $current_user));

$ids = array();
foreach ($friends_notables as $row)
{
foreach (array('to_user_id', 'from_user_id') as $field)
{
if (FALSE == in_array($row[$field], $ids))
{
$ids[] = $row[$field];
}
}
}
$people = People::get_many($ids);

$smarty->assign('friends_notables', $friends_notables);
$smarty->assign('people', $people);
$smarty->assign('criteria', $_GET['criteria']);
$smarty->display('friends.tpl');
?>




And now, have fun using and developing OpenSocial applications.

MySpace OpenSocial Disaster


It's been less than 24 hours since MySpace OpenSocial platform opened their doors. While Facebook and Bebo value their developers, provide support and rely on their feedback, MySpace is obviously different.

So what do we have so far?

- Angry developers complaining about the fact that MySpace "suspended" their applications without explanation. Some received vague reasons for their apps being denied, like:

"Application logo violates copy rights issue. This logo is belong to google talk."
"Ads in the application navigate the site away from MySpace"
"Application is not working fine."

Judging by not-so-perfect English it looks like the review process was outsourced overseas?

According to the developer community forum there is no way to edit/test or re-submit suspended applications right away.

Great start.