保证正确的Ajax调用参数值的最佳方法


Best way to guarantee correct Ajax calls parameter values

我在一个网站上工作,需要一些ajax调用,以提高灵活性和性能。我的ajax调用是一个排名系统。我需要用ajax处理三个输入值(storeID、clientID、orderID)。要使用ajax提交操作,我要确保发送的参数值没有被使用web工具的用户修改。因此,我考虑了三种不同的方法来保证发送的信息不被更改:

  1. 发送一个额外的值,该值是所有发送的数据的加密。因此,在服务器端处理ajax时,我可以重新加密发送的数据,并查看加密结果是否与发送的加密值匹配。

  2. 将所有数据作为一个加密值发送。然后在服务器上执行ajax时,我可以解密数据并再次分配值。

  3. 只发送orderID及其加密,然后使用方法(1)验证orderID未被更改,并使用数据库查询获取另外两个信息。

以下是我对这三种方式的看法:

  1. 消耗内存,因为我必须发送orderID, clientID, storeID, encryptedID。此外,ajax调用中监控的信息将为人们提供有关他们对订单进行评级时发生的情况的信息。

  2. 我在网上检查了mcrypt_encrypt和mcrypt_decrypt,但我从未使用过它们。我看到它们生成了一个很长的字符串,但是我更喜欢保持我的数据发送短,或者看起来像md5加密的数据。有没有更好的方法?

  3. 这是一种优雅的方式,它看起来很直接,但它需要一些MySQL干预,这可能会消耗时间,特别是当数据在未来增长时。

那么你认为哪一个更好?如果你有更多的方法,我很感激你在这里分享。谢谢你

我想避免的场景示例:单击按钮将通过传递Product ID使用AJAX提交表单。用户进入源代码,将产品ID从X更改为Y,用户点击按钮,提交表单,ID Y的产品受到影响。可以看到,发送的参数值是不安全的,可以修改。我正在寻找一种方法来保证发送的参数值是正确的,不修改。

PS:这个问题不是关于CSRF处理的。

每个人都试图引导你到这些点:

在服务器上....

  1. 您知道用户是谁(auth)。
  2. 应该定义用户可以访问的内容(ACL)。
  3. 您应该验证所请求的产品既存在,又允许用户访问。
  4. 如果允许用户对产品进行更改,那么根据定义,他们有权更改X产品,如果他们对Y产品信息进行更改,因为他们在源代码中捣乱,那么这是用户的责任。
  5. 如果这是与购物车相关的,那么您应该忽略用户传递的所有其他数据(例如,价格),并仅通过ID加载产品数据。所以他们不能以X产品的价格购买Y产品。然后,如果他们在Firebug中输入Y产品的ID,谁在乎呢?

如果你不做第二和第三,你完全是在浪费时间。无论你想到什么,都只会让你的不安全感更加模糊。

如果您想使用加密,您可以将Mcrypt函数与Rijndael密码一起使用。这给了你一个128位的块来玩。

假设您的三个id (storeID, clientID, orderID)每个都是32位,您可以将它们与4个字符串一起打包以形成128位块。该字符串将用于稍后检查解密的数据。

$block = pack('a4LLL', 'MyID', $storeID, $clientID, $orderID);

然后创建一个随机IV并加密块:

$mod = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
$iv = openssl_random_pseudo_bytes(mcrypt_enc_get_iv_size($mod));
$mySecretKey = 'LA72U/aLEOcDoN0rqtM5sehNNjd7TUSILiBgI8bej6o=';
mcrypt_generic_init($mod, base64_decode($mySecretKey), $iv);
$encrypted = mcrypt_generic($mod, $block);
mcrypt_generic_deinit($mod);
mcrypt_module_close($mod);

在客户端使用加密块和IV之前,您需要对它们进行编码。使用base64并交换/和+字符可以得到一个可以作为GET参数发送的不错的值:

$encodedBlock = substr(strtr(base64_encode($encrypted), '/+', '-_'), 0, -2);
$encodedIV = substr(strtr(base64_encode($iv), '/+', '-_'), 0, -2);

之后,当这些值被发送到ajax脚本时,只需反转这个过程:

$encrypted = base64_decode(strtr($_GET['block'], '-_', '/+') . '==');
$iv = base64_decode(strtr($_GET['iv'], '-_', '/+') . '==');
$mod = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
mcrypt_generic_init($mod, base64_decode($mySecretKey), $iv);
$block = mdecrypt_generic($mod, $encrypted);
mcrypt_generic_deinit($mod);
mcrypt_module_close($mod);
$data = unpack('a4str/Lstore/Lclient/Lorder', $block);
if ($data['str'] != 'MyID')
  throw new Exception('Something fishy is going on');

如果一切顺利,$data将包含$data['store'], $data['client']和$data['order']。

你也可以将nonce值打包到块中,以防止重放攻击。

一种相当简单的方法是根据服务器上保存的密钥(并且永远不会以任何方式向用户暴露)对三个参数的基本前向(即sha/md5等)散列进行salt

然后,当您收到用户的请求时,您可以简单地复制这个仅向前的散列过程,以确保数据未被篡改,类似于大多数站点实现身份验证的方式,而无需在其数据库中存储用户密码的明文副本。

例如:

define('SUPER_SECRET_KEY', 'foobar123'); // our 007 secret key to hash requests against
function generateToken($storeID, $clientID, $orderID) {
    return hash('sha256', SUPER_SECRET_KEY . "/{$storeID}/{$clientID}/{$orderID}");
}
function validateToken($token, $storeID, $clientID, $orderID) {
    return generateToken($storeID, $clientID, $orderID) === $token;
}

就像我说的,这是一个非常基本的起点。使用静态salt值不会非常有效,并且您仍然会让自己打开重播攻击(即有人一遍又一遍地发送相同的令牌/clientID/storeID/orderID组合)。

对抗重放攻击的一种方法是为每个请求生成一个nonce作为salt

另一个使用nonces的超级基本示例,前面有大量伪代码:

function generateNonce() {
    return hash('sha512', mt_rand() . time()); // please make a more secure nonce generator than this :)
}
// We need a way to retrieve the nonce for a particular request:
function generateLookup($storeID, $clientID, $orderID) {
    return hash('sha256', "{$storeID}/{$clientID}/{$orderID}");
}
function generateToken($nonce, $storeID, $clientID, $orderID) {
    $lookup = generateLookup($storeID, $clientID, $orderID);
    // Store the $nonce and $lookup somewhere (i.e a database)
    $db->query("INSERT INTO ... (nonce,lookup,expired) VALUES ('$nonce','$lookup','N')");
    return hash('sha256', "{$nonce}/$storeID/$clientID/$orderID");
}
function validateToken($token, $storeID, $clientID, $orderID) {
    $lookup = generateLookup($storeID, $clientID, $orderID);
    $rows = $db->query("SELECT nonce FROM ... WHERE lookup = '$lookup' AND expired = 'N'"); // again, pseudocode for retrieving the nonce from the persistent store
    if (count($rows) !== 0) {
        $nonce = $rows[0]['nonce'];
        // expire the nonce (note: these database queries should be in a transaction)
        $db->query("UPDATE ... SET expired = 'Y' WHERE $lookup = '$lookup' AND nonce = '$nonce'");
        return generateToken($nonce, $storeID, $clientID, $orderID) === $token;
    }
    return false;
}

那么你的基本工作流程将变成:

上得到

$data = // retrieve data
$token = generateToken(generateNonce(), $data['storeID'], $data['clientID'], $data['orderID']);
// output the $token value in a hidden field in your form
柱子上

if (validateToken($_POST['token'], $_POST['storeID'], $_POST['clientID'], $_POST['orderID'])) {
    // All good
}
else {
    echo "Back, ye filthy hacker!";
}

我真的不认为加密对你有帮助。

似乎你担心这里是一个已经登录的用户摆弄请求参数(正常的会话安全应该保持用户没有登录)。

我认为你只需要在服务器端验证请求。您应该能够通过会话变量识别您的用户。然后,ajax服务可以以正常的方式验证请求。

为了安全起见,你需要保持简单。

这里的简单事实是任何客户端内容都可以被修改。

所以你需要的是一个服务器端检查。

服务器生成原始表单,并将此表单设置在专用于特定用户的页面上。此表单应该有一些CSRF令牌,以确保发布的数据来自服务器构建的页面,而不是来自其他网站,而不是来自一些自动脚本等。

因此,第一步是确保所有表单都包含一些令牌,并且服务器构建的所有表单都可以在一定时间内检查这些令牌,并确保令牌与先前生成的用户/表单匹配。这意味着需要在服务器端缓存表单之类的东西,并在此缓存上执行一些严肃的清理任务,并且还需要处理包含过时令牌的发布表单的消息(在这里使用ajax)(对不起,您等了太久)。一些更简单的方法只使用用户会话来存储反csrf令牌,您需要找到一个满足您所有需求的解决方案(匿名表单、共享表单、多步骤表单等)

一旦有了一个反csrf令牌系统,在服务器端存储防止用户修改表单元素的东西就很容易了。像"id"这样的东西可以简单地存储在表单缓存中,甚至不发送到客户端。您还可以存储一些表单元素的签名,等等

您毫无理由地走了一条艰难的路。

你几乎不应该在浏览器的请求和下面的请求之间强行建立链接。正确的逻辑(REST的基础)应该是

    根据请求,向浏览器发送与orderID相关的信息
  • 处理浏览器的修改请求,而不用担心以前的请求:只需使用数据库检查修改请求是否有效和一致,在这种情况下处理它

从DB中获取两个值不应该让你担心:如果它很慢,这意味着你没有做错,可以解决。

我认为最好的方法是从发送的值作为哈希值(如md5)发送一个额外的值,然后在服务器中使用相同的算法只是比较发送的哈希值与生成的值,如果它等于然后发布的数据是有效的。

让我们假设你使用一个javascript md5库,只需计算哈希值:

hash=md5('sometext'+storeID+clientID+orderID);

然后用AJAX提交这个哈希,最后在服务器上再次计算并比较。

我看到的唯一问题是,如果工作是在客户端完成的,用户可以很容易地得到哈希算法,计算它并发送。我猜你会使用一些混淆技术。

也可以生成一个随机数,包括它在md5和发送一个额外的参数。这将每次生成不同的散列。

永远不要相信客户。任何东西都可以修改,而且假设它们最终能够猜出您在服务器端实现了什么也更安全。所以,我的建议是,只发送能让你确定所做决定的信息。

例如,如果表单一次只应用于一个产品,则将值存储在服务器端(例如在$SESSION变量中)。当他们提交表单时,它只能应用于该产品,并且只会影响他们对UI的操作。

如果clientID特定于用户,则存储在服务器端,并且根本不将其发送到客户端。

如果他们可以用UI改变客户端或产品,那么他们是否修改脚本就无关紧要了。

既然你似乎已经有了某种身份验证机制,只要工作,如果他们有权限做某事,谁在乎他们是否破解客户端来做它,如果他们可以通过UI来做。如果他们没有得到许可,一开始就不要让他们接触到这些信息。当然,隐藏他们一开始就不应该知道的数据可能看起来很安全,但这绝对不如不发送他们一开始就不应该知道的数据安全,无论你把它弄得多么模糊。

编辑:根据其他答案的评论,有很多方法可以实现这一点,但这里有一个让你思考:

你可以很容易地处理多个表单打开一次:使用mcrypt_create_iv生成一个随机令牌。比如说,长度为20个字符。将其存储在会话数组

$formtoken = mcrypt_create_iv(20);
$_SESSION['infos'][$formtoken] = array($value,$anothervalue);

然后将$formtoken发送到浏览器。每次浏览器通过AJAX提交时,发送一个新的表单令牌并删除与前一个令牌一起存储的数据,因此它不再作为提交有效。您需要发送的唯一信息是令牌,用户无法操纵它,因为它是在每次请求表单时生成的,并且只对该表单有效,而其他数据无论如何都可以操作。

首先,http无论如何都不安全。因此:

请使用https保护客户端之间的通信服务器。

通过使用https,例如可以保护用户的会话不被劫持

如果使用ajax从客户端传输到服务器的数据对隐私不敏感,我不会建议加密它。

保持非隐私相关数据不加密。

使用这种策略,没有人会猜到,某些敏感的(例如跟踪)相关数据以隐藏的方式传输。传输非隐私相关的非加密数据不会损害用户对的信任

来自客户端的数据不安全

当然,您的服务器应用程序不能信任来自客户端的数据。客户端可能是

  • 合法用户
  • 合法用户的电脑被木马劫持
  • 或从第三方注入的数据包。

验证client -> server -> client周期

要验证来自客户端的数据是否真的来自客户端,需要客户端传回一些服务器提供的数据。

假设允许客户端使用http传输某些值V1, V2, ...,这些值在传递给服务器之前。

如果请求是通过正常的直接点击浏览器链接或间接ajax调用是不相关的。

使用静态服务器端秘密签名

由于值V1, V2, ..., Vn来自服务器,因此将值签名为来自服务器的应用程序。

使用服务器端的秘密TheSecret,例如:

SignValue := md5( V1 + V2 + ... Vn + TheSecret )

当然,您可以使用其他单向哈希。对于大多数实际情况,md5可能足够强大。

使用临时服务器端秘密签名

除了一个固定的秘密TheSecret,您可能希望每天生成一个新的秘密。DailySecret可能像这样生成:

DailySecret := TheSecret  + md5( TheSecret + date( 'Ymd' ) )
SignValue := md5( V1 + V2 + ... Vn + DailySecret )

如果你的ajax发送了那么多的敏感信息,你需要改变你的设计

但是如果你不能这样做,你也可以在服务器上散列值,你的ajax只接受散列值。如果客户端更改了哈希值,那么它就无效了。这是防止id操纵的标准做法。