Use two-domain design for more secure communication with external content

In this article, we use the Wikipedia app for Office to show how to communicate between two domains to help address security concerns in apps for Office and SharePoint.

The Wikipedia app is an open source project, and you can download the source code from Wikipedia App for Office source (GitHub).

Use two domains for better security

Apps for Office and SharePoint communicate with external resources and data. In the same domain, unauthorized users might take advantage of the same-origin policy by adding malicious code to the external resources or data, and then using the code to steal information from your file or inside your SharePoint site. To avoid this, you can use a two-domain design. The domains communicate through a message-posting function, and then actions are taken by the event listeners. For each domain, there is a set of message-posting function and event listeners.

Wikipedia app two-domain implementation

Wikipedia is an open platform that lets anyone add and update content. In designing the Wikipedia app for Office, we considered security and the potential for attacks from Wikipedia. The search actions from Wikipedia sites should be separated from actions like accessing the Office client (Word or Excel). The app uses two domains we call the Wikipedia domain and the Sandbox domain. The code that interacts with Wikipedia is in the Sandbox domain.

In each domain, we use the DomainNamepostmessage function to communicate with another domain and the DomainNameeventlistener function to complete tasks. Here DomainName is either sandbox or wikipedia.

Note again that all the following source code can be downloaded from Wikipedia App for Office source (GitHub).

For example, in the file Wikipedia_utils.js of the Wikipedia domain, we use the sandboxPostMessage function to communicate with the Sandbox domain. The communication is through the Sandbox domain’s URL (sandBoxHostURl).

 sandboxPostMessage: function (message, showLoading) {
  if (showLoading) {
    UI.showLoadingScreen();
  }

  SandboxInteraction.resetMessageIndex();
  var messageSent = { 
    "message": message, 
    "sequence": ++SandboxInteraction.messageSequenceNo 
  };
  
  document.getElementById(
    Selectors.sandboxClassName
    ).contentWindow.postMessage(
        JSON.stringify(messageSent), SandboxInteraction.sandBoxHostURl
  );
  SandboxInteraction.sandboxTimeout = setTimeout(
    "UI.writeError(Errors.sandboxServiceDown, ErrorType.error)", 
    30000
  );
},

We use the sandboxEventListener function to take actions after communicating with the Sandbox domain.

 sandboxEventListener: function (event) {
  if (event.origin !== SandboxInteraction.sandBoxHostURl) {
    return;
  } else {
    clearTimeout(SandboxInteraction.sandboxTimeout);
  }

  var messageJson;
  try{
    messageJson = JSON.parse(event.data);
  } catch (err) {
    UI.writeError(Errors.jsonParseError, ErrorType.error);
  }

  SandboxInteraction.sandboxSequenceNo = messageJson.sequence;

  if (SandboxInteraction.sandboxSequenceNo !== 
    SandboxInteraction.messageSequenceNo) {
    return;
  }

  switch (messageJson.message.callback) {
    case "updateSectionCallback":
      if (!GlobalVars.nextState.isNewArticle) {
        GlobalVars.nextState = State.newSectionView();
      }

      GlobalVars.nextState.article = messageJson.message.content;

      if (GlobalVars.nextState.isNewArticle) {
        GlobalVars.nextState.article += 
          CodeSnippet.toggleArticleButton();
      } else {
        GlobalVars.nextState.article += 
          CodeSnippet.backToMainArticleButton();
        GlobalVars.nextState.tocReady = true;
      }

      GlobalVars.nextState.articleReady = true;
      GlobalVars.nextState.writeToPage(true);
      UI.showFullArticleButton();
      UI.hideLoadingScreen();

      break;
    case "updateTocCallback":
      if(messageJson.message.referenceID && messageJson.message.referenceID
        !== 0) {
        GlobalVars.nextState.referenceID = messageJson.message.referenceID;
      }

      GlobalVars.nextState.topic = decodeURIComponent(
                                      messageJson.message.redirectTitle);
      GlobalVars.nextState.toc = messageJson.message.toc;
      GlobalVars.nextState.tocReady = true;
      GlobalVars.nextState.type = State.TypeEnum.article;
      GlobalVars.nextState.writeToPage(true);

      break;
    case "expandArticleCallback":
      UI.writeToArticleExpansionContainer(messageJson.message.content);
      UI.hideLoadingScreen();

      break;
    case "updateImagesGridCallback":
      if (GlobalVars.nextState.type === State.TypeEnum.images
          && GlobalVars.currState.type !== State.TypeEnum.images) {
        GlobalVars.nextState.writeToPage(true);
      }

      if (GlobalVars.nextState.type !== State.TypeEnum.images
          && GlobalVars.currState.type !== State.TypeEnum.images) {
        SandboxInteraction.sandboxPostMessage("cancel");

        return;
      }

      $(Selectors.article).append(messageJson.message.content);
      UI.processSection($(Selectors.article).children().last());

      break;
    case "updateFullSizedPictureCallback":
      // The only reason that a full size image is fetched is 
      // inserting the image to the document
      Client.insertImage(
        CodeSnippet.insertedImage(
          messageJson.message.imgUrl, 
          messageJson.message.fileName, 
          messageJson.message.detailUrl
        )
      );

      break;
    case "updateReferenceCallback":
      GlobalVars.nextState = State.newSectionView();
      GlobalVars.nextState.type = State.TypeEnum.reference;
      GlobalVars.nextState.article = messageJson.message.content;
      GlobalVars.nextState.articleReady = true;
      GlobalVars.nextState.writeToPage(true);

      break;
    case "updateCategoryCallback":
      GlobalVars.nextState.article = messageJson.message.content;
      GlobalVars.nextState.toc = StrUtil.empty;
      GlobalVars.nextState.tocReady = true;
      GlobalVars.nextState.articleReady = true;
      GlobalVars.nextState.type = State.TypeEnum.category;
      GlobalVars.nextState.writeToPage(true);

      break;
    case "searchWikiCallback":
      UI.hideFullArticleButton();
      GlobalVars.nextState.article = messageJson.message.content;
      GlobalVars.nextState.articleReady = true;
      GlobalVars.nextState.tocReady = true;
      GlobalVars.nextState.type = State.TypeEnum.search;
      GlobalVars.nextState.writeToPage(true);

      break;
    case "searchCancel":
      UI.hideLoadingScreen();

      break;
    case "noResult":
      GlobalVars.nextState.topic = GlobalVars.currState.topic;
      GlobalVars.nextState.isNewArticle = false;
      UI.writeError(
        messageJson.message.errorMessage,
        ErrorType.notFoundResult
      );

      break;
    case "error":
      UI.writeError(messageJson.message.errorMessage, ErrorType.error);

      break;
  }
},

Based on the message type, six corresponding functions are developed to take actions.

 postSearchArticleMessage: function (title) {
  var message = { "function": "updateArticle", "title": title };
  SandboxInteraction.sandboxPostMessage(message, true);
},

postEntireArticleMessage: function (title) {
  var message = { "function": "expandArticle", "title": title };
  SandboxInteraction.sandboxPostMessage(message, true);
},

postSectionMessage: function (title, sectionID) {
  var message = { 
    "function": "updateSection", 
    "title": title, 
    "sectionID": sectionID 
  };
  SandboxInteraction.sandboxPostMessage(message, true);
},

postImageMessage: function (title) {
  var message = { "function": "updateImagesGrid", "title": title };
  SandboxInteraction.sandboxPostMessage(message, true);
},

postFullSizedImageMessage: function (title, maxImageWidth) {
  var message = { 
    "function": "updateFullSizedPicture", 
    "title": title, 
    "maxImageWidth": maxImageWidth 
  };
  SandboxInteraction.sandboxPostMessage(message, false);
},

postReferenceMessage: function (title, sectionID) {
  var message = { 
    "function": "updateReference", 
    "title": title, 
    "sectionID": sectionID 
  };
  SandboxInteraction.sandboxPostMessage(message, true);
}

The communication is through JSON with Padding (JSONP).

Similarly, in the Sandbox domain, we use the wikipediaPostMessage function to communicate with the Wikipedia domain and use wikipediaEventListener to take actions after the communication in the sandbox.js file. The communication is through the Wikiepedia domain’s URL (wikipediaBoxHostURl).

 wikipediaPostMessage: function (message, sequenceNo) {
  if (WikipediaAppInteraction.wikipediaSequenceNo === sequenceNo) {
    var messageSent = { "message": message, "sequence": sequenceNo };
    parent.postMessage(
      JSON.stringify(messageSent), 
      WikipediaAppInteraction.wikipediaHostURL
    );
  }
},

Here the events are divided into six types, and different content information is prepared to send to the Wikipedia domain.

 wikipediaEventListener: function (event) {
  if (event.origin !== WikipediaAppInteraction.wikipediaHostURL) {
    return;
  }

  var messageJson;

  try{
    messageJson = JSON.parse(event.data);
  } catch (err) {
    postErrorMessage(Errors.JsonParseError, WikipediaAppInteraction + 1);
  }

  WikipediaAppInteraction.wikipediaSequenceNo = messageJson.sequence;

  switch (messageJson.message.function) {
    case "updateArticle":
      WikipediaAppInteraction.wikipediaSearchTitle = 
        messageJson.message.title;

      if (Wikipedia.isCategoryQuery(
            WikipediaAppInteraction.wikipediaSearchTitle
         )) {
        Wikipedia.getCategoryMembers(
          WikipediaAppInteraction.wikipediaSearchTitle, 
          JSONParser.updateCategoryCallback
        );
      } else {
        // Fetch the introduction section of the article from wikipedia
        Wikipedia.getHTMLBySectionAsync(
          WikipediaAppInteraction.wikipediaSearchTitle, 
          Wikipedia.INTRODUCTION_SECTION_ID, 
          JSONParser.updateSectionCallback
        );
        // Fetch the table-of-content section of the article from wikipedia
        Wikipedia.getTableOfContentAsync(
          WikipediaAppInteraction.wikipediaSearchTitle, 
          JSONParser.updateTocCallback
        );
      }

      break;
    case "expandArticle":
      Wikipedia.getEntireArticle(
        messageJson.message.title, 
        JSONParser.expandArticleCallback
      );

      break;
    case "updateSection":
      WikipediaAppInteraction.wikipediaSearchTitle = 
        messageJson.message.title;

      Wikipedia.getHTMLBySectionAsync(
        messageJson.message.title, 
        messageJson.message.sectionID, 
        JSONParser.updateSectionCallback
      );

      break;
    case "updateImagesGrid":
      Wikipedia.getImagesForArticle(
        messageJson.message.title, 
        JSONParser.updateImagesGridCallback
      );

      break;
    case "updateFullSizedPicture":
      Wikipedia.getImage(
        messageJson.message.title, 
        messageJson.message.maxImageWidth, 
        JSONParser.updateFullSizedPictureCallback
      );

      break;
    case "updateReference":
      Wikipedia.getReferenceSectionForArticle(
        messageJson.message.title, 
        messageJson.message.sectionID, 
        JSONParser.updateReferenceCallback
      );

      break;
  }
},

References

Attribution

This article was written by content publisher Tony Liu after reviewing the code together with Microsoft ecosystem team SDET Tao Wu, SDE Xiangyue Hua, and program manager Ruoying Liang. Senior program manager Humberto Lezama Guadarrama provided valuable feedback.