From my colleague Reid at http://blog.sforce.com/sforce/2011/05/application-initialization-patterns-from-forcecom-labs.html

"I've had a few people ask me about using the standard Visualforce "apex:page" tag and action="doSomething" attribute.  I've stayed away from this in these apps as the docs clearly state this particular action should not be used for initialization."

The chief reason for this is to help prevent a class of attacks called "Cross-Site Request Forgery" or CSRF's.  More information can be found at


For those of you familiar with CSRFs, you might consider that to be an overly broad prohibition.  "Of course I can write <apex:page> actions that don't do anything interesting when CSRF'd", you might say.  And you'd be right!  If you've got a good idea what a CSRF is, and you're completely fine with the security implications of a CSRF-able page, please feel free to use <apex:page> action attributes.  If you have no idea what a CSRF is, or you don't know if it's safe or not to leave your page CSRF-able, read on.

So what's a CSRF?  Consider an online shopping website, which for this post we'll call "llamazon.biz", where you can buy high-quality Peruvian Llamas and have them drop-shipped anywhere on earth with the touch of a button.  http://www.llamazon.biz/buy-llama.html might have a form on it that looks like…

 
<form id="quality-llamas" action="/buy.asp" method="POST">
   <input type="text" name="shipping_address"></input>
   <input type="text" name="quantity"></input>
   <select name="llama_type">
      <option value="city">City Llama</option>
      <option value="country">Country Llama</option>
      <option value="coolranch">Zesty Cool Ranch Llama</option>
   </select>
   <input type="submit" value="Submit">Buy it!</input>
</form>
  
 In regular use, the user is already logged in to llamazon.biz, so they just need to write in the shipping address, the number of llamas to buy, and pick which variety of Llama you want.  llamazon.biz helpfully keeps your credit card number on file in their database.
Now, the problem here is that form posts aren't subject to the browser's same-origin policy.  That means, if you're logged in to llamazon.biz and your browser has llamazon.biz cookies, but the above <form/> is served up from http://www.evil-hacker.com/evil-llama.html, then when the user clicks "submit", the browser will still submit the request to llamazon.biz and helpfully include the user's llamazon.biz cookies.  There's no way for the llamazon server to tell if the user submitted the request from the llamazon page or the evil-hacker page.

But it gets worse!  With just a tiny amount of javascript, the user doesn't even have to click anything.  The evil-hacker.com page could look like…

  
<form id="llamas-to-pgh" action="https://www.llamazon.biz/buy.asp" method="POST">
   <input type="hidden" name="shipping_address" value="5000+Forbes+Ave.%2CPittsburgh%2C+PA%2+C15213"/>
   <input type="hidden" name="quantity" value="100" />
   <input type="hidden name="llama_type" value="3" />
   <input type="hidden" value="Submit" />
</form>
<script>document.getElementById("llamas-to-pgh").submit();</script>
 
 And just by loading the evil-hacker.com page, the innocent user will have asked llamazon.biz to send 100 Zesty Cool Ranch Llamas to Pittsburgh.  As an aside, note that JavaScript isn't the only way to do this.  The same sort of cross-domain post is possible with Adobe Flash (see AS3's navigateToURL() method, which allows POSTs, and lots of other arbitrary HTTP headers to be added).  You should also note that the attacker who owns evil-hacker.com doesn't need to know anything about the innocent user.  They never know the  innocent user's cookies.  They never know the innocent user's home address, or credit card number.  And they never see the result of the form submission.  The attacker relies on the browser using the cookies on evil-hacker's behalf.
So how do we fix this?  Why is Pittsburgh not already overrun with Llamas?  There are a couple of ways, but the industry-standard best practice to this is something called a CSRF "token".  With the token approach, we require the form to include something that the attacker doesn't know, and can't guess.  In the <form id="quality-llamas">, that would be an extra <input> tag that looks like…

<input type="hidden" name="_CONFIRMATIONTOKEN" value="xxyyzz" />

Where "xxyyzz" is usually something like a SHA-256 hash of the user's llamazon.biz cookie concatenated with some other stuff.  The computation happens on the server-side, and the browser doesn't have to do any math.  It just echoes the string back on form submission.  It's usually not the llamazon.biz cookie itself, but a hash of it, for a variety of reasons.  Consider what happens if the user's machine is shared by a family.  If mom logs out and dumps her cookies, the llamazon.biz page itself might still be in cache.  Then if snoopy little brother trawls through the cache, he can steal the cookie and buy llamas on mom's credit card, or view mom's purchase history, or any number of inappropriate things, since llamazon.biz will think little brother *is* mom.

The token approach works because the attacker at evil-hacker never actually knows the value of the user's cookie.  They just rely on the browser using it on their behalf.  Because they don't know the cookie, they can't compute the value of the token.  Because they can't compute the token, they can't include it in their malicious <form id="llamas-to-pgh">.  Because the server re-computes the token when it receives the form and compares the re-computed token to the form-submitted token, that comparison will fail and no llamas get shipped to Pittsburgh.

So that's a CSRF, and how to mitigate against it.  What does that mean to you as a VisualForce/Apex developer?

Whenever you use <apex:form>, the platform automatically creates a CSRF token for you, includes it the <form> HTML element that's sent to the browser, and validates it when the form is submitted.  If the validation fails, we don't execute the form, or any action attribute you've assigned to it.  That's not the case when you use an action on <apex:page> – there are no CSRF tokens auto-created there.

Here’s an example of how you might run into this pattern.  llamazon.biz is a loyal Salesforce.com customer, which they use for inventory control.  Specifically, llamas are recorded as standard Contact objects with one slight change; there’s a custom field named “llama_type__c”.  It stores a picklist with valid strings “city”, “country”, and “coolranch”.

Since llamazon only deals with the highest-quality llama, they employ a crack team of highly trained caretakers, each of whom personally looks after a dozen or so llamas.  In our example, the caretakers are Salesforce users.  Further, each llama Contact object has its “owner” field set to the caretaker.  The llamas also tend to form very close emotional bonds to one another, so whenever they move from one location to another (ah, the fast-paced life of a jet-setting llama), most of the time all of a caretaker’s charges will all move together.  This results in the caretakers doing lots of bulk mailing address update changes.

Hey, wouldn’t it be cool if there was a Force.com page that let the caretakers do a bulk update of mailing addresses of all their llamas?  That would be awesome!  Oh, but here we run into our unsafe code example.  The VF page looks like this:


<apex:page controller="Llamas" action="{!update_llamas_mailing}">
   <h1>you have {!LlamaCount} llama{!LlamaPlural}</h1>
   <apex:dataTable value="{!llamas}" var="l"  border="1">
      <apex:column value="{!l.LastName}"/>
      <apex:column value="{!l.Llama_Type__c}"/>
      <apex:column value="{!l.MailingStreet}"/>
      <apex:column value="{!l.MailingCity}"/>
      <apex:column value="{!l.MailingState}"/>
      <apex:column value="{!l.MailingCountry}"/>
   </apex:dataTable>
</apex:page>
 
The custom Apex controller looks like this:
 
public with sharing class Llamas {
   public final List<Contact> llamas {get; set; }
   private final Integer llama_count {get; set;}
   private final Map<String, String> p;

   public Llamas() {
      this.llamas = [select id, LastName, MailingStreet, MailingCity, MailingState, MailingPostalCode, MailingCountry, llama_type__c from Contact where Llama_Type__c != NULL];
      this.llama_count = this.llamas.size();
      this.p = ApexPages.currentPage().getParameters();
   }
 
   public Integer getLlamaCount(){ return this.llamas.size(); }

   public String getLlamaPlural(){

      return this.llamas.size() == 1 ? '' : 's';
   }

   public void update_llamas_mailing() {
      Map<String, String> p = this.p;
      for(Contact c : this.llamas){
         if(p.get('MailingStreet') != null ){

            c.MailingStreet = p.get('MailingStreet'); }
         if(p.get('MailingCity') != null ){
            c.MailingCity = p.get('MailingCity'); }
         if(p.get('MailingState') != null ){
            c.MailingState = p.get('MailingState'); }
         if(p.get('MailingPostalCode') != null ){
            c.MailingPostalCode = p.get('MailingPostalCode'); }
         if(p.get('MailingCountry') != null ){
            c.MailingCountry = p.get('MailingCountry'); }
      }
      update this.llamas;
   }
 
   static testMethod void myTest(){
      Llamas test_llama = new Llamas();
      System.assert(test_llama.getLlamaCount() >= 0);
   }
}
 
So what does all that do?  On page load, the constructor fires and the this.llama Map is filled in with all of the caretaker’s llama Contacts.  Then, the update_llama_mailing() method fires, it iterates through each of and it inspects the URL query parameters for any mailing address values.  If it finds any, it updates the llama Contact objects with the new mailing addresses.  For example, if the URL looked like…

https://llamazon.na9.force.com/apex/llamas?mailingcity=Pittsburgh&mailingpostalcode=15213

then update_llama_mailing() would set the MailingCity to Pittsburgh and the MailingPostalCode to 15213 for all of the caretaker’s llamas.  The caretaker would also get a nice little table listing all of his charges and their new mailing addresses.

And there’s the flaw!  There’s nothing that prevents an attacker from crafting an evil-hacker.com page that contains a bunch of image tags that look like…

<img src=”https://llamazon.na9.force.com/apex/llamas?mailingcity=Pittsburgh&…”>

And if the attacker can trick the caretaker from viewing evil-hacker.com (not even clicking anything, just loading the evil-hacker.com page), then the poor caretaker’s browser will happily send a cookie of to salesforce, and the llamas are off to spend a sultry summer in Pittsburgh.

The solution, as discussed above, is to move the action from <apex:page> to <apex:form>.  The new VisualForce page looks like…

 
<apex:page controller="Llamas">
   <h1>you have {!LlamaCount} llama{!LlamaPlural}</h1>
   <apex:dataTable value="{!llamas}" var="l"  border="1">
      <apex:column value="{!l.LastName}"/>
      <apex:column value="{!l.Llama_Type__c}"/>
      <apex:column value="{!l.MailingStreet}"/>
      <apex:column value="{!l.MailingCity}"/>
      <apex:column value="{!l.MailingState}"/>
      <apex:column value="{!l.MailingPostalCode}"/>
      <apex:column value="{!l.MailingCountry}"/>
   </apex:dataTable>
   <apex:form >
      <apex:pageBlock title="Bulk Update Llama Mailing Address">
         <apex:messages />
         <apex:pageBlockButtons >
            <apex:commandButton value="Save" action="{!update_llamas_mailing}"/>
         </apex:pageBlockButtons>
         Street: <apex:inputText value="{!fl.MailingStreet}" /><p />
         City: <apex:inputText value="{!fl.MailingCity}" /><p />
         State: <apex:inputText value="{!fl.MailingState}" /><p />
         Postal Code: <apex:inputText value="{!fl.MailingPostalCode}" /><p />
         Country: <apex:inputText value="{!fl.MailingCountry}" /><p />
      </apex:pageBlock>
   </apex:form>
</apex:page>
 
with Apex code…
 
public with sharing class Llamas {
   public final List<Contact> llamas {get; set; }
   private final Integer llama_count {get; set;}
   public Map<String, String> p {get; set; }
   public Contact fl {get; set; }  // fake llama!

   public Llamas() {
      this.llamas = [select id, LastName, MailingStreet, MailingCity, MailingState, MailingPostalCode, MailingCountry, llama_type__c from Contact where Llama_Type__c != NULL];
      this.llama_count = this.llamas.size();
      this.p = ApexPages.currentPage().getParameters();
      this.fl = new Contact();
   }
 
   public Integer getLlamaCount(){
      return this.llamas.size();
   }

   public String getLlamaPlural(){
      return this.llamas.size() == 1 ? '' : 's';
   }

   public void update_llamas_mailing() {
      Map<String, String> p = this.p;
   
      for(Contact c : this.llamas){
         if(fl.MailingStreet != null) { c.MailingStreet = fl.MailingStreet; }
         if(fl.MailingCity != null) { c.MailingCity = fl.MailingCity; }
         if(fl.MailingState != null) { c.MailingState = fl.MailingState; }
         if(fl.MailingPostalCode != null) { c.MailingPostalCode = fl.MailingPostalCode; }
         if(fl.MailingCountry != null) { c.MailingCountry = fl.MailingCountry; }
      }
      update this.llamas;
   }
 
   static testMethod void myTest(){
      Llamas test_llama = new Llamas();
      System.assert(test_llama.getLlamaCount() >= 0);
   }
}

 
Here, rather than inspect the query parameters directly, we create a “fake” llama Contact object, and have the VF page prompt the caretaker to enter in the new mailing address in the page, not as query parameters.  Of course, under the covers, those mailing address fields become POST parameters and are saved into the “fl” Contact object.  However, the platform is handling that for us via the <apex:inputText> tag, and that “fl” fake llama isn’t persisted into the database after the page is done loading.
Most importantly, if you view source on the page before submitting, you’ll see something like this..
<input type="hidden"  id="com.salesforce.visualforce.ViewStateCSRF" name="com.salesforce.visualforce.ViewStateCSRF" value="[big-ugly-long-base64-string]" />

That big-ugly-long-base64-string?  That’s the CSRF token, which the platform has also auto-provided us with, as discussed above.  If it’s absent (or incorrect) when the page is requested, the platform won’t call update_llamas_mailing(), and we’re safe from CSRF.

So why are we so capricious?  Why do we protect you on <apex:form> but not <apex:page>?  The problem is that if we required valid tokens for <apex:page>, we'd break direct navigation.  Right now, I can take the link https://llamazon-app.na1.force.com/00A0000abcdef that points to some object, and mail it or IM it to coworkers, and they can just click it and load it in their browser.  If the force.com platform were to require a valid token for all <apex:page>'s, I'd have no way to do that sort of regular casual link sharing we all take for granted.

Now, as I said above, you may (rightly) be thinking that there are a whole slew of <apex:page> actions you could do that it doesn't matter if an attacker can trick a user into executing.  If your action just does a basic SOQL query for inclusion on the page, but doesn't actually create, update, or delete any objects in the database (or do ANYTHING ELSE), you're fine.  Remember, the attacker in a CSRF is blind – they never see the results of the request, so they can't eavesdrop on it, either.

When you're trying to decide if something needs CSRF protection, the question you should always ask yourself is:

"If this results in something happening that makes an innocent user say 'But I didn't do that!', you need a CSRF token"

While creating, updating, and deleting database objects covers most of that ground, it doesn't cover everything.  If your action does any HTTP callouts (like say, a 3rd party API), and that results in something happening in the 3rd party's database, you need a CSRF token.  If your action code sends mail, you need a CSRF token.  If your object is sensitive, and you're under a regulatory requirement to have audit log records every time the object is viewed (not just edited), you should really, really have a CSRF token to help maintain the integrity of the audit log.

Get the latest Salesforce Developer blog posts and podcast episodes via Slack or RSS.

Add to Slack Subscribe to RSS