是否可以限制javascript函数的范围?

时间:2012-01-26 15:02:38

标签: javascript security

假设我在全局范围内有变量。

假设我希望定义一个我可以保证无法访问此变量的函数,有没有办法包装函数或调用函数,这将确保这个?

实际上,我需要任何规定的函数来定义对变量的访问权限,并且该访问要在该函数定义之前定义,并与该函数定义分开。

动机: 我正在考虑用户提交功能的可能性。我应该能够相信这个功能是各种各样的“安全”,因此很乐意在我自己的网站上发布它们。

11 个答案:

答案 0 :(得分:18)

在托管在不同Origin上的iframe中运行代码。这是保证的唯一方法,即不受信任的代码被沙箱化并阻止访问全局变量或页面的DOM。

答案 1 :(得分:10)

使用嵌入式Web Workers 可以允许运行安全功能。这样的东西允许用户输入javascript,运行它并获得结果而无需访问您的全局上下文。



globalVariable = "I'm global";

document.getElementById('submit').onclick = function() {
  createWorker();
}


function createWorker() {
  // The text in the textarea is the function you want to run
  var fnText = document.getElementById('fnText').value;

  // You wrap the function to add a postMessage 
  // with the function result
  var workerTemplate = "\
function userDefined(){" + fnText +
    "}\
postMessage(userDefined());\
onmessage = function(e){console.log(e);\
}"

  // web workers are normally js files, but using blobs
  // you can create them with strings.
  var blob = new Blob([workerTemplate], {
    type: "text/javascript"
  });

  var wk = new Worker(window.URL.createObjectURL(blob));
  wk.onmessage = function(e) {
    // you listen for the return. 
    console.log('Function result:', e.data);
  }

}

<div>Enter a javascript function and click submit</div>
<textarea id="fnText"></textarea>
<button id="submit">
  Run the function
</button>
&#13;
&#13;
&#13;

您可以尝试这些,例如在textarea中粘贴它:

return "I'm a safe function";

你可以看到它的安全:

return globalVariable;

你甚至可以拥有更复杂的脚本,如下所示:

var a = 4, b = 5;
function insideFn(){
    // here c is global, but only in the worker context
    c = a + b;
}
insideFn();
return c;

在此处查看有关Web工作者的信息,尤其是嵌入式Web工作者: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#Embedded_workers

答案 2 :(得分:5)

有点晚了,但也许它会帮助你一点

function RestrictFunction(params) {

    params = ( params == undefined ? {} : params );
    var scope = ( params.scope == undefined ? window : params.scope );
    var data = ( params.data == undefined ? {} : params.data );
    var script = ( params.script == undefined ? '' : params.script );
    if (typeof params.script == 'function') {
        script = params.script.toString();
        script = script.substring(script.indexOf("{") + 1, script.lastIndexOf("}"));
        }

    // example: override native functions that on the white list

    var setTimeout = function(_function,_interval) {

        // this is important to prevent the user using `this` in the function and access the DOM
        var interval = scope.setTimeout( function() { 
            RestrictFunction({
                scope:scope,
                data:data,
                script:_function
                });
            } , _interval );

        // Auto clear long user intervals
        scope.setTimeout( function() {
            scope.clearTimeout(interval);
            } , 60*1000 );

        return interval;
        }       

    // example: create custom functions

    var trace = function(str) {
        scope.console.log(str);
        }   

    return (function() {

        // remove functions, objects and variables from scope

        var queue = [];
        var WhiteList = [
            "Blob","Boolean","Date","String","Number","Object","Array","Text","Function",
            "unescape","escape","encodeURI","encodeURIComponent","parseFloat","parseInt",
            "isNaN","isFinite","undefined","NaN",
            "JSON","Math","RegExp",
            "clearTimeout","setTimeout"
            ];

        var properties = Object.getOwnPropertyNames(scope);
        for (var k = 0; k<properties.length; k++ ) {
            if (WhiteList.indexOf(properties[k])!=-1) continue;
            queue.push("var "+properties[k]+" = undefined;");
            }   

        for (var k in scope) {
            if (WhiteList.indexOf(k)!=-1) continue;
            queue.push("var "+k+" = undefined;");
            }

        queue.push("var WhiteList = undefined;");   
        queue.push("var params = undefined;")   ;
        queue.push("var scope = undefined;")    ;
        queue.push("var data = undefined;") ;
        queue.push("var k = undefined;");   
        queue.push("var properties = undefined;");  
        queue.push("var queue = undefined;");   
        queue.push("var script = undefined;");  
        queue.push(script); 

        try {
        return eval( '(function(){'+ queue.join("\n") +'}).apply(data);' ); 
        } catch(err) { }

        }).apply(data);

    }   

使用示例

// dummy to test if we can access the DOM
var dummy = function() {

    this.notify = function(msg) {
        console.log( msg );
        };

    }

var result = RestrictFunction({

    // Custom data to pass to the user script , Accessible via `this`
    data:{
        prop1: 'hello world',
        prop2: ["hello","world"],
        prop3: new dummy()
        },

    // User custom script as string or function
    script:function() {

        trace( this );

        this.msg = "hello world";
        this.prop3.notify(this.msg);

        setTimeout( function() {
            trace(this); 
            } , 10 );

        trace( data );
        trace( params );
        trace( scope );
        trace( window );
        trace( XMLHttpRequest );
        trace( eval );

        return "done!"; // not required to return value...

        },

    }); 

console.log( "result:" , result );

答案 3 :(得分:4)

您可以使用WebWorkers来隔离您的代码:

  

创建一个完全独立的并行执行环境(即单独的线程或进程或等效构造),并在该上下文中异步运行其余步骤。

这是一个简单的例子:

class SkypeProtocolDateTime;
class SkypeProtocol
{
public:
    SkypeProtocol();
    virtual ~SkypeProtocol(){}

private:
    class SkypeProtocolDateTime* m_pimpl;
};

此代码将返回:

someGlobal = 5; //As a worker normally take another JavaScript file to execute we convert the function in an URL: http://stackoverflow.com/a/16799132/2576706 function getScriptPath(foo) { return window.URL.createObjectURL(new Blob([foo], { type: 'text/javascript' })); } function protectCode(code) { var worker = new Worker(getScriptPath(code)); } protectCode('console.log(someGlobal)'); // prints 10 protectCode('console.log(this.someGlobal)'); protectCode('console.log(eval("someGlobal"))'); protectCode('console.log(window.someGlobal)');

Uncaught ReferenceError: someGlobal is not defined

undefined

Uncaught ReferenceError: someGlobal is not defined

所以你现在的代码是安全的。

答案 4 :(得分:3)

我将以至少一种可能性为您的问题提供技术答案。使用global的名称作为该函数的参数:

someGlobal = 5;

function cantSeeThatGlobal(someGlobal) {
  console.log(someGlobal);
}

cantSeeThatGlobal();   // prints undefined
cantSeeThatGlobal(10); // prints 10

当然只是不使用全局变量会更好。

答案 5 :(得分:3)

您不能使用“call”或“apply”方法限制函数的范围,但是您可以使用“eval”和作用域的简单技巧从本质上隐藏要调用的函数中的任何特定全局变量

这是因为该函数可以访问在函数本身所声明的范围内声明的“全局”变量。因此,通过复制方法的代码并将其注入eval,您实际上可以更改要调用的函数的全局范围。最终结果基本上可以在一定程度上沙箱化一段javascript代码。

这是一个完整的代码示例:

<html>
<head>
<title>This is the page title.</title>
<script>
    function displayTitle()
    {
        alert(document.title);
    }

    function callMethod(method)
    {
        var code = "" +
            // replace global "window" in the scope of the eval
            "var window = {};" +
            // replace global "document" in the scope of the eval
            "var document = {}; " +
            "(" +

            // inject the Function you want to call into the eval
                method.toString() +

            // call the injected method
            ")();" +
            "";
        eval(code);
    }

    callMethod(displayTitle);
</script>
</head>
<body></body>
</html>

获得eval'd的代码如下所示:

var window = {};
var document = {};
(function displayTitle()
{
    alert(document.title);
})();

答案 6 :(得分:3)

创建一个具有相同名称的局部变量。 如果您有这样的全局变量:

var globalvar;

在你的功能中:

function noGlobal(); {
    var globalvar;
}

如果函数引用globalvar,它将引用本地函数。

答案 7 :(得分:3)

编辑:此答案不会隐藏window.something变量。但它有一种运行用户定义代码的简洁方法。我试图找到掩盖窗口变量的方法

您可以使用javascript函数Function.prototype.bind()将用户提交的函数绑定到您选择的自定义范围变量,在此自定义范围中,您可以选择要与用户定义的函数共享哪些变量,以及要隐藏哪些变量。对于用户定义的函数,代码将能够使用this.variableName访问您共享的变量。这是一个详细说明这个想法的例子:

&#13;
&#13;
// A couple of global variable that we will use to test the idea
var sharedGlobal = "I am shared";
var notSharedGlobal = "But I will not be shared";

function submit() {
  // Another two function scoped variables that we will also use to test
  var sharedFuncScope = "I am in function scope and shared";
  var notSharedFuncScope = "I am in function scope but I am not shared";

  // The custom scope object, in here you can choose which variables to share with the custom function
  var funcScope = {
    sharedGlobal: sharedGlobal,
    sharedFuncScope: sharedFuncScope
  };

  // Read the custom function body
  var customFnText = document.getElementById("customfn").value;
  // create a new function object using the Function constructor, and bind it to our custom-made scope object
  var func = new Function(customFnText).bind(funcScope);

  // execute the function, and print the output to the page. 
  document.getElementById("output").innerHTML = JSON.stringify(func());

}

// sample test function body, this will test which of the shared variables   does the custom function has access to. 
/* 
return {
        sharedGlobal : this.sharedGlobal || null,
         sharedFuncScope : this.sharedFuncScope || null,
       notSharedGlobal : this.notSharedGlobal || null,
         notSharedFuncScope : this.notSharedFuncScope || null
 }; 
*/
&#13;
<script type="text/javascript" src="app.js"></script>
<h1>Add your custom body here</h1>
<textarea id="customfn"></textarea>
<br>
<button onclick="submit()">Submit</button>
<br>
<div id="output"></div>
&#13;
&#13;
&#13;

该示例执行以下操作:

  1. 接受用户的功能正文
  2. 当用户点击提交时,该示例使用Function constructor从自定义正文创建一个新的功能对象。在示例中,我们创建了一个没有参数的自定义函数,但是可以轻松添加params作为Function构造函数的第一个输入
  3. 执行该功能,并将其输出打印在屏幕上。
  4. 示例函数体包含在注释中,用于测试自定义函数可以访问哪些变量。

答案 8 :(得分:0)

据我所知,在Javascript中,在函数外部声明的任何变量都属于全局范围,因此可以从代码中的任何位置访问。

每个函数都有自己的作用域,并且只能从该函数和任何嵌套函数访问该函数中声明的任何变量。 JavaScript中的本地范围仅由函数创建,也称为函数范围。

将函数放在另一个函数中可能是一种可以实现缩小范围(即嵌套范围)的可能性

答案 9 :(得分:0)

如果您要谈论的是通过加载第三方脚本公开给您的功能,那么您就很不走运了。那是因为函数的作用域是在定义它的源文件中定义的。当然,您可以将它绑定到其他东西,但是在大多数情况下,如果它需要调用其他函数或触摸任何数据,那将使该函数无用。在它自己的源文件中-仅当您可以预测它需要能够访问并可以自己访问的内容时,更改其范围才是切实可行的-如果第三方脚本触摸了在闭包,对象内定义的数据或不在您范围内的功能,您将无法模拟可能需要的功能。

如果您有权访问源文件,则它非常简单-查看源文件,查看其是否尝试访问该变量,然后编辑代码以使其无法访问。

,但是假设您已加载函数,并且不需要与“窗口”以外的任何东西进行交互,并且出于学术原因,您想执行此操作,这是一种方法-制作一个沙箱使其可以播放这是一个简单的填充程序包装,它按名称排除了某些项目

function suspectCode() {
    console.log (window.document.querySelector("#myspan").innerText);
    console.log('verbotten_data:',typeof verbotten_data==='string'?verbotten_data:'<BANNED>');
    console.log('secret_data:',typeof secret_data==='string'?secret_data:'<BANNED>');    // undefined === we can't
    console.log('window.secret_data:',typeof window.secret_data==='string'?window.secret_data:'<BANNED>'); 
    
    secret_data = 'i changed the secret data !';
    console.log('secret_data:',typeof secret_data==='string'?secret_data:'<BANNED>');    // undefined === we can't
    console.log('window.secret_data:',typeof window.secret_data==='string'?window.secret_data:'<BANNED>'); 
}

var verbotten_data = 'a special secret';

window.secret_data = 'special secret.data';


console.log("first call the function directly");
suspectCode() ;

console.log("now run it in a sandbox, which banns 'verbotten_data' and 'secret_data'");
runFunctionInSandbox (suspectCode,[
    'verbotten_data','secret_data', 
    
    // we can't touch items tied to stack overflows' domain anyway so don't clone it
    'sessionStorage','localStorage','caches',
    
    // we don't want the suspect code to beable to run any other suspect code using this method.
    'runFunctionInSandbox','runSanitizedFunctionInSandbox','executeSandboxedScript','shim',
    
    ]);

function shim(obj,forbidden) {
   const valid=Object.keys(obj).filter(function(key){return forbidden.indexOf(key)<0;});
   var shimmed = {};
   valid.forEach(function(key){
       try {
         shimmed[key]=obj[key];
       } catch(e){
           console.log("skipping:",key);
       }
   });
   return shimmed;
}

function fnSrc (fn){
  const src = fn.toString();
  return src.substring(src.indexOf('{')+1,src.lastIndexOf('}')-1);
}

function fnArgs (fn){
  let src = fn.toString();
  src = src.substr(src.indexOf('('));
  src = src.substr(0,src.indexOf(')')-1);
  src = src.substr(1,src.length-2);
  return src.split(',');
}


function runSanitizedFunctionInSandbox(fn,forbidden) {
    const playground = shim(window,forbidden);
    playground.window = playground;
    let sandboxed_code = fn.bind(playground,playground.window);
    sandboxed_code();
}

function runFunctionInSandbox(fn,forbidden) {
   
   const  src  = fnSrc(fn);
   const  args = fnArgs(fn);
   executeSandboxedScript(src,args,forbidden);
}

function executeSandboxedScript(sourceCode,arg_names,forbidden) {
   var script = document.createElement("script");
   script.onload = script.onerror = function(){ this.remove(); };
   script.src = "data:text/plain;base64," + btoa(
       [
            'runSanitizedFunctionInSandbox(function(',
            arg_names,
            ['window'].concat(forbidden),
            '){ ',
            sourceCode,
            '},'+JSON.stringify(forbidden)+')'
       ].join('\n')
    );
   document.body.appendChild(script);
}
<span id="myspan">Page Access IS OK<span>

或更复杂的版本,允许将参数传递给函数

 
function suspectCode(argument1,argument2) {
    console.log (window.document.querySelector("#myspan").innerText);
    console.log(argument1,argument2);
    console.log('verbotten_data:',typeof verbotten_data==='string'?verbotten_data:'<BANNED>');
    console.log('secret_data:',typeof secret_data==='string'?secret_data:'<BANNED>');    // undefined === we can't
    console.log('window.secret_data:',typeof window.secret_data==='string'?window.secret_data:'<BANNED>'); 
    
    secret_data = 'i changed the secret data !';
    console.log('secret_data:',typeof secret_data==='string'?secret_data:'<BANNED>');    // undefined === we can't
    console.log('window.secret_data:',typeof window.secret_data==='string'?window.secret_data:'<BANNED>'); 
    
    
}

var verbotten_data = 'a special secret';

window.secret_data = 'special secret.data';


console.log("first call the function directly");
suspectCode('hello','world') ;

console.log("now run it in a sandbox, which banns 'verbotten_data' and 'secret_data'");
runFunctionInSandbox (suspectCode,['hello','sandboxed-world'],
    [
    'verbotten_data','secret_data', 
    
    // we can't touch items tied to stack overflows' domain anyway so don't clone it
    'sessionStorage','localStorage','caches',
    
    // we don't want the suspect code to beable to run any other suspect code using this method.
    'runFunctionInSandbox','runSanitizedFunctionInSandbox','executeSandboxedScript','shim',
    
    ]);

function shim(obj,forbidden) {
   const valid=Object.keys(obj).filter(function(key){return forbidden.indexOf(key)<0;});
   var shimmed = {};
   valid.forEach(function(key){
       try {
         shimmed[key]=obj[key];
       } catch(e){
           console.log("skipping:",key);
       }
   });
   return shimmed;
}

function fnSrc (fn){
  const src = fn.toString();
  return src.substring(src.indexOf('{')+1,src.lastIndexOf('}')-1);
}

function fnArgs (fn){
  let src = fn.toString();
  src = src.substr(src.indexOf('('));
  src = src.substr(0,src.indexOf(')'));
  src = src.substr(1,src.length);
  return src.split(',');
}


function runSanitizedFunctionInSandbox(fn,args,forbidden) {
    const playground = shim(window,forbidden);
    playground.window = playground;
    let sandboxed_code = fn.bind(playground,playground.window);
    sandboxed_code.apply(this,new Array(forbidden.length).concat(args));
}

function runFunctionInSandbox(fn,args,forbidden) {
   const  src  = fnSrc(fn);
   const  arg_names = fnArgs(fn);
   executeSandboxedScript(src,args,arg_names,forbidden);
}

function executeSandboxedScript(sourceCode,args,arg_names,forbidden) {
   var script = document.createElement("script");
   script.onload = script.onerror = function(){ this.remove(); };
   let id = "exec"+Math.floor(Math.random()*Number.MAX_SAFE_INTEGER).toString();
   window.execArgs=window.execArgs||{};
   window.execArgs[id]=args;
   let script_src = [
                            'runSanitizedFunctionInSandbox(function(',
                            ['window'].concat(forbidden),
                            (arg_names.length===0?'':','+arg_names.join(","))+'){',
                            sourceCode,
                            '},',
                            'window.execArgs["'+id+'"],',
                             JSON.stringify(forbidden)+');',
                            'delete window.execArgs["'+id+'"];'
                       ].join('\n');
                       
   let script_b64 = btoa(script_src);
   script.src = "data:text/plain;base64," +script_b64;
   document.body.appendChild(script);
}
<span id="myspan">hello computer...</span>

答案 10 :(得分:0)

我验证了@ josh3736的答案,但他没有留下例子

这里是要验证其工作的

parent.html

<h1>parent</h1>

<script>
abc = 'parent';
function foo() {
  console.log('parent foo: abc = ', abc);

}
</script>

<iframe></iframe>

<script>
const iframe = document.querySelector('iframe');
iframe.addEventListener('load', function() {
  console.log('-calling from parent-');
  iframe.contentWindow.foo();
});
iframe.src = 'child.html';

</script>

child.html

<h1>
child
</h1>

<script>
abc = 'child';

function foo() {
  console.log('child foo: abc = ', abc);
}

console.log('-calling from child-');
parent.foo();
</script>

运行时它会打印

-calling from child-
parent foo: abc = parent
-calling from parent-
child foo: abc = child

孩子和父母都有一个变量abc和一个函数foo。 当子级调用父级的foo时,父级中的函数将看到父级的全局变量,而当父级调用子级的foo时,该函数将看到子级的全局变量。

这也适用于评估。

parent.html

<h1>parent</h1>

<iframe></iframe>

<script>
const iframe = document.querySelector('iframe');
iframe.addEventListener('load', function() {
  console.log('-call from parent-');
  const fn = iframe.contentWindow.makeFn(`(
      function() {
        return abc;
      }
  )`);
  console.log('from fn:', fn());
});
iframe.src = 'child.html';

</script>

child.html

<h1>
child
</h1>

<script>
abc = 'child';

function makeFn(s) {
  return eval(s);
}
</script>

运行时它会打印

-call from parent-
from fn: child

表明它看到了孩子的abc变量而不是父母的

注意:如果您以编程方式创建iframes,则似乎必须将其添加到DOM中,否则它们将不会加载。例如

function loadIFrame(src) {
  return new Promise((resolve) => {
    const iframe = document.createElement('iframe');
    iframe.addEventListener('load', resolve);
    iframe.src = src;
    iframe.style.display = 'none';
    document.body.appendChild(iframe);  // iframes don't load if not in the document?!?! 
  });
}

当然,在上面的孩子中,我们看到孩子可以接触到父级,因此该代码为 NO SANDBOXED 。如果您想确保孩子不能回来,您可能必须添加一些东西来隐藏访问父级的各种方式,但是至少作为开始,您显然可以使用此技术为代码提供不同的全局范围。

还请注意,iframe当然必须与父级位于同一个域中。