Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:124796 X-Original-To: internals@lists.php.net Delivered-To: internals@lists.php.net Received: from php-smtp4.php.net (php-smtp4.php.net [45.112.84.5]) by qa.php.net (Postfix) with ESMTPS id D602A1A00B7 for ; Tue, 6 Aug 2024 08:54:29 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1722934571; bh=igSLk95EzEmbeaWbwRLEO3once6v/IIKuSvU1AN0x0Q=; h=Date:From:To:In-Reply-To:References:Subject:From; b=C61BVM9ToGsQeWk3E2LDKftj+yreOXFI6jTpUUkdKAAC+4gEt018e0qIrN7qY1aFd +9aQxBjScw7+VBOtcC3qelpEqa2XDN6FeACh7a6qrYE+f+rIqMPbLd4BOX+ZSKJFa3 rxNIH+0CPwnazUXgLRTfKjWcdGsBadZFEkK8qRK3kaDcYPDIj/Y0e/CUXe8skSwHCM VAWQG5UPZ7bRiyO0YyjnT4QhDBY4Yf16MU1M9MIvpkG1nMsv/O+wgYioLsBCumjg4P 3GQ+7ml0v7+lbXtsPDx0x1B5ATOcS4G5PR1STgJmpppiNPRmE7/4LYI9oBAgiYccA8 PNetrq/0uh8GQ== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id E8A89180080 for ; Tue, 6 Aug 2024 08:56:09 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 4.0.0 (2022-12-13) on php-smtp4.php.net X-Spam-Level: X-Spam-Status: No, score=-0.1 required=5.0 tests=BAYES_50,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,DMARC_MISSING,HTML_MESSAGE, RCVD_IN_DNSWL_LOW,RCVD_IN_MSPIKE_H4,RCVD_IN_MSPIKE_WL,SPF_HELO_PASS, SPF_PASS autolearn=no autolearn_force=no version=4.0.0 X-Spam-Virus: No X-Envelope-From: Received: from fhigh3-smtp.messagingengine.com (fhigh3-smtp.messagingengine.com [103.168.172.154]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by php-smtp4.php.net (Postfix) with ESMTPS for ; Tue, 6 Aug 2024 08:56:09 +0000 (UTC) Received: from compute3.internal (compute3.nyi.internal [10.202.2.43]) by mailfhigh.nyi.internal (Postfix) with ESMTP id A52F91151B4E for ; Tue, 6 Aug 2024 04:54:27 -0400 (EDT) Received: from imap49 ([10.202.2.99]) by compute3.internal (MEProxy); Tue, 06 Aug 2024 04:54:27 -0400 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=bottled.codes; h=cc:content-type:content-type:date:date:from:from:in-reply-to :in-reply-to:message-id:mime-version:references:reply-to:subject :subject:to:to; s=fm1; t=1722934467; x=1723020867; bh=yILzPXLvcE Un9FP41xoiEkQ55mmJ8BdgJF2g2aAzPn8=; b=LydDWtfQGFIOnOtuJABa7aVZ10 +c0nOFrIWGSujzc0Hg68WZnED9EqfBK8pKct/91HnvVIQLdFs3xquBVML+gJasIz RzU1Gvg+vbogy/aRHk7GVMaJEsOXsLJsJHIMsOvw7Zyvto2Eo8ujfZ+oFEMbaQm1 q0GFkJbPVLXToaMJwhWGh+6/EDlgEPCq3FxgG02wWe4mZbQz+fnl3gC7g3tX3fZS sUBkXTWvTCQ6lvTMaZFojozsyLYVhaH40EUbUvetAQz0TGBQ1BzGRR9DDzQZYM+J PEFyKEzRTPYBO1u0P3k1KaOb3PqMMldQ+hhmLYQQgL1tA/BbCYQjzZmNadcA== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:content-type:content-type:date:date :feedback-id:feedback-id:from:from:in-reply-to:in-reply-to :message-id:mime-version:references:reply-to:subject:subject:to :to:x-me-proxy:x-me-proxy:x-me-sender:x-me-sender:x-sasl-enc; s= fm3; t=1722934467; x=1723020867; bh=yILzPXLvcEUn9FP41xoiEkQ55mmJ 8BdgJF2g2aAzPn8=; b=NtYbJLdKAhJWPsU9WLgwIHJgfGvD1h+YP9qhGSkj+DYi odX1PJxP21RMsjScM/lUEpljc7l2pArxUTQyRSjN/Iw7M5xQ3m4iZmDjIJs1LU7D UbNSgFVVTlkUZk2+rGVRMBvGfvPhvW6rWxoTWW6offCYTcX5uQ8IloBUo9xvSxxn M5tUqYwkGwGlXX1fPbb9O6MXPDthcTcG/kErq87HNrNF9BBk+KyT78xVDA52kEL9 CeK+WmUFhgnVjVqxXjRrkOB6xb5DHoPpta1O8d6WonPZXak+2PUZTggx4qb+GJ3Z UnvMa+c1FdTjjkIkyllHtlkz6xEc1AAtnIpEjIQDNw== X-ME-Sender: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgeeftddrkeekgddtkecutefuodetggdotefrodftvf curfhrohhfihhlvgemucfhrghsthforghilhdpqfgfvfdpuffrtefokffrpgfnqfghnecu uegrihhlohhuthemuceftddtnecunecujfgurhepofggfffhvffkjghfufgtsegrtderre ertdejnecuhfhrohhmpedftfhosgcunfgrnhguvghrshdfuceorhhosgessghothhtlhgv ugdrtghouggvsheqnecuggftrfgrthhtvghrnhepleeuvefhvdefudeitdelveeiteefle fffeegheefjeffveekhfduieekueekfeegnecuffhomhgrihhnpegvgigrmhhplhgvrdgt ohhmnecuvehluhhsthgvrhfuihiivgeptdenucfrrghrrghmpehmrghilhhfrhhomheprh hosgessghothhtlhgvugdrtghouggvshdpnhgspghrtghpthhtoheptd X-ME-Proxy: Feedback-ID: ifab94697:Fastmail Received: by mailuser.nyi.internal (Postfix, from userid 501) id 45A1E15A0092; Tue, 6 Aug 2024 04:54:27 -0400 (EDT) X-Mailer: MessagingEngine.com Webmail Interface Precedence: bulk list-help: list-post: List-Id: internals.lists.php.net x-ms-reactions: disallow MIME-Version: 1.0 Date: Tue, 06 Aug 2024 10:54:07 +0200 To: internals@lists.php.net Message-ID: <32236410-e4ad-4c63-b7e7-cdbd3832bbc1@app.fastmail.com> In-Reply-To: References: Subject: Re: [PHP-DEV] [Discussion] Sandbox API Content-Type: multipart/alternative; boundary=a24101511b5145478470261418c83560 From: rob@bottled.codes ("Rob Landers") --a24101511b5145478470261418c83560 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable On Tue, Aug 6, 2024, at 10:41, Nick Lockheart wrote: >=20 > Sandbox: Security >=20 > A SandBox has two use cases: >=20 > 1. Unit Testing of code with mocks or stubs, and also, allowing testing > with different environments. >=20 > 2. The secure running of 3rd party code inside a 1st party application. >=20 >=20 > For the second use case, I will use a fictional blogging software > called "Hot Blog" as the example. >=20 > Hot Blog is a very popular Open Source blogging platform. Hot Blog > allows third party developers to write plugins. >=20 > While many Hot Blog plugin developers have the best of intentions, some > of them are novice coders that make security mistakes. >=20 > So let's talk about how Hot Blog could benefit from using the new > SandBox API. >=20 > By default, a SandBox instance is a blank slate. There's nothing inside > of it, unless the SandBox is told to have something in it. >=20 > That means that sandboxed code that tries to read $_SESSION will find > an empty array. Same with $_SERVER, $_POST, and $_GET. >=20 > That's by default. This allows the code that controls the sandbox to > create custom access to application level resources. >=20 > Let's say that Hot Blog wants plugin developers to be able to access > certain $_POST variables, but only *after* Hot Blog has checked the > strings for multi-byte attack vulnerabilities. >=20 > To do this, Hot Blog creates a class called PluginAPI with a > GetCleanPost method. This lets sandboxed plugins get $_POST data > without being able to bypass Hot Blog's mandatory security check. > (Remember, $_POST is empty inside the sandbox). >=20 > The code looks like this: >=20 > $oSandbox =3D new SPLSandBox(); > $oSandbox->MockClass('\HotBlog\PluginAPI','\HotBlog\PluginAPI'); > $oUserPlugin =3D $oSandbox->GetInstance('BobsMagicPlugin'); > $oUserPlugin->Run(); >=20 > Because "Bob" has written his plugin as a Hot Blog plugin and knows > that Hot Blog's rules require him to use > \HotBlog\PluginAPI::GetCleanPost() to access a $_POST variable, he > calls that instead of using $_POST. >=20 > Now, Hot Blog can impose mandatory security checks on incoming data > making their application more secure. >=20 > Next, let's talk about includes. By default, if sandboxed code tries to > include or require a file, a SandBoxAccessViolation is thrown. >=20 > Letting sandboxed code include whatever it wants defeats the point of a > sandbox, at least for security use cases. >=20 > Of course, includes are useful, and plugins may need them. But the > outer application should be able to control that access. >=20 > Enter SPLSandBox::RegisterIncludeHandler(). >=20 > RegisterIncludeHandler accepts a callable. >=20 > The callable's signature is: >=20 > (int $IncludeType, string $FileName, string $FilePath) >=20 > Where: >=20 > $IncludeType is: > 0 - require > 1 - require_once > 2 - include > 3 - include_once >=20 > $FileName is the file without the path, and $FilePath is the path with > trailing `/`. >=20 > If the sandbox should allow includes, the sandbox should have an > Include Handler registered. >=20 > The SandBox API calls the include handler, if defined, when sandboxed > code tries to include or require files. >=20 > Let's setup a function so our plugin authors can include files, but > only from their own plugin directory: >=20 > // Sandbox setup for includes: >=20 > $oSandbox =3D new SPLSandBox(); > $oSandbox->RegisterIncludeHandler('HotBlogInclude'); >=20 > $oUserPlugin =3D $oSandbox->GetInstance('BobsMagicPlugin'); > $oUserPlugin->Run(); >=20 >=20 > // Include Handler: >=20 > function HotBlogInclude($Type, $FileName, $FilePath){ > =20 > if(file_exists($PluginDirectoy.$FileName)){ > $oSandbox->Include($PluginDirectoy.$FileName); > return 0; > } > return 1; // error! > } >=20 > In the above example, $FilePath contained the path that Bob requested > with his include statement. But we ignored it! Bob is only allowed to > include from his plugin's own directory, so we see if the file is in > $PluginDirectoy instead. >=20 > If the file is in Bob's directory, we include it *into* the sandbox > with SPLSandBox::Include(), making it available to Bob's code, but > keeping the main application code clean of any registrations the > include may cause. >=20 >=20 > ** Back to Unit Testing ** >=20 > For the Unit Testing use case, however, certain code under test may > normally read from $_GET, and that shouldn't change under test. >=20 > In this next example, we are running a unit test on a FrontController > class, and we want to see if it works with many different URL > structures. >=20 > Normally, the web server will map example.com/a/b/c to $_GET vars, so > the FrontController class expects something like: >=20 > $_GET =3D [ > 'a' =3D> 'Forum', > 'b' =3D> 'Post' > 'c' =3D> '123' > ]; >=20 > Let's make sure our FrontController is doing everything right with a > battery of tests: >=20 > $aControllerTests =3D [ > ['Forum','Post','123'], > ['Blog','Post','123'], > ['Article','acb'], > ['Cart','Product','723'], > ['Cart','Category','Jeans'] > ]; >=20 > $aTestResults =3D []; > foreach($aControllerTests as $TestID =3D> $GetVars){ >=20 > $oSandbox =3D new SPLSandBox(); > $oSandbox->MockGlobal('$_GET',$GetVars); =20 > $oController =3D $oSandbox->GetInstance('FrontController'); > $aTestResults[$TestID] =3D $oController->Init(); > $oSandbox->Destroy(); > } >=20 > SPLSandBox::MockGlobal() lets us set global variables (including super > globals) inside the sandbox. >=20 > Now, $aTestResults contains the results of each test, run with separate > $_GET parameters. >=20 > With this structure, you could get **every** valid URL from a database > and run a unit test with custom $_GET params on your FrontController to > make sure everything works. >=20 Hey Nick, This looks quite valuable, and I assume auto loading would work just lik= e normal? Register an autoloader that will eventually require the file a= nd call this function? It would be nice to provide a simplified api as well, maybe =E2=80=9CCop= yCurrentEnvironment()=E2=80=9D or something? In most cases, it is easie= r/faster to find things to remove vs. adding everything on every plugin/= request every time.=20 In saying that, it would be great if there was an api for =E2=80=9Cshari= ng=E2=80=9D a base-sandbox pool via shm (or similar to a pool) so that t= he base vm doesn=E2=80=99t need to be recreated potentially hundreds of = times per request.=20 =E2=80=94 Rob --a24101511b5145478470261418c83560 Content-Type: text/html; charset=utf-8 Content-Transfer-Encoding: quoted-printable

=
On Tue, Aug 6, 2024, at 10:41, Nick Lockheart wrote:
<= /div>

= Sandbox: Security

A SandBox has two use cas= es:

1. Unit Testing of code with mocks or s= tubs, and also, allowing testing
with different environmen= ts.

2. The secure running of 3rd party code= inside a 1st party application.


=
For the second use case, I will use a fictional blogging software
called "Hot Blog" as the example.

<= div>Hot Blog is a very popular Open Source blogging platform. Hot Blog
allows third party developers to write plugins.

While many Hot Blog plugin developers have the best of= intentions, some
of them are novice coders that make secu= rity mistakes.

So let's talk about how Hot = Blog could benefit from using the new
SandBox API.

By default, a SandBox instance is a blank slate. T= here's nothing inside
of it, unless the SandBox is told to= have something in it.

That means that sand= boxed code that tries to read $_SESSION will find
an empty= array. Same with $_SERVER, $_POST, and $_GET.

<= div>That's by default. This allows the code that controls the sandbox to=
create custom access to application level resources.
<= /div>

Let's say that Hot Blog wants plugin developers= to be able to access
certain $_POST variables, but only *= after* Hot Blog has checked the
strings for multi-byte att= ack vulnerabilities.

To do this, Hot Blog c= reates a class called PluginAPI with a
GetCleanPost method= . This lets sandboxed plugins get $_POST data
without bein= g able to bypass Hot Blog's mandatory security check.
(Rem= ember, $_POST is empty inside the sandbox).

The code looks like this:

$oSandbox =3D ne= w SPLSandBox();
$oSandbox->MockClass('\HotBlog\PluginAP= I','\HotBlog\PluginAPI');
$oUserPlugin =3D $oSandbox->G= etInstance('BobsMagicPlugin');
$oUserPlugin->Run();
=

Because "Bob" has written his plugin as a Hot = Blog plugin and knows
that Hot Blog's rules require him to= use
\HotBlog\PluginAPI::GetCleanPost() to access a $_POST= variable, he
calls that instead of using $_POST.

Now, Hot Blog can impose mandatory security checks = on incoming data
making their application more secure.
=

Next, let's talk about includes. By default, i= f sandboxed code tries to
include or require a file, a San= dBoxAccessViolation is thrown.

Letting sand= boxed code include whatever it wants defeats the point of a
sandbox, at least for security use cases.

Of course, includes are useful, and plugins may need them. But the
<= /div>
outer application should be able to control that access.

Enter SPLSandBox::RegisterIncludeHandler().
<= /div>

RegisterIncludeHandler accepts a callable.
<= /div>

The callable's signature is:

=
(int $IncludeType, string $FileName, string $FilePath)

Where:

$IncludeType = is:
0 - require
1 - require_once
2 - include
3 - include_once

$FileName is the file without the path, and $FilePath is the path wit= h
trailing `/`.

If the sandbo= x should allow includes, the sandbox should have an
Includ= e Handler registered.

The SandBox API calls= the include handler, if defined, when sandboxed
code trie= s to include or require files.

Let's setup = a function so our plugin authors can include files, but
on= ly from their own plugin directory:

// Sand= box setup for includes:

$oSandbox =3D new S= PLSandBox();
$oSandbox->RegisterIncludeHandler('HotBlog= Include');

$oUserPlugin =3D $oSandbox->G= etInstance('BobsMagicPlugin');
$oUserPlugin->Run();
=


// Include Handler:

function HotBlogInclude($Type, $FileName, $FilePath){<= br>
  
   if(file_exists($Pl= uginDirectoy.$FileName)){
      $= oSandbox->Include($PluginDirectoy.$FileName);
 &nb= sp;    return 0;
   }
   return 1; // error!
}

In the above example, $FilePath contained the path that Bob req= uested
with his include statement. But we ignored it! Bob = is only allowed to
include from his plugin's own directory= , so we see if the file is in
$PluginDirectoy instead.
=

If the file is in Bob's directory, we include = it *into* the sandbox
with SPLSandBox::Include(), making i= t available to Bob's code, but
keeping the main applicatio= n code clean of any registrations the
include may cause.


** Back to Unit Testing **

For the Unit Testing use case, however, certa= in code under test may
normally read from $_GET, and that = shouldn't change under test.

In this next e= xample, we are running a unit test on a FrontController
cl= ass, and we want to see if it works with many different URL
structures.

Normally, the web server will= map example.com/a/b/c to = $_GET vars, so
the FrontController class expects something= like:

$_GET =3D [
 &nbs= p; 'a' =3D> 'Forum',
   'b' =3D> 'Post'
   'c' =3D> '123'
];

Let's make sure our FrontController is doing everything= right with a
battery of tests:

$aControllerTests =3D [
   ['Forum','Post','1= 23'],
   ['Blog','Post','123'],
&n= bsp;  ['Article','acb'],
   ['Cart','Produc= t','723'],
   ['Cart','Category','Jeans']
];

$aTestResults =3D [];
<= div>foreach($aControllerTests as $TestID =3D> $GetVars){

   $oSandbox =3D new SPLSandBox();
<= div>   $oSandbox->MockGlobal('$_GET',$GetVars);  =  
   $oController =3D $oSandbox->GetInst= ance('FrontController');
   $aTestResults[$TestI= D] =3D $oController->Init();
   $oSandbox->= ;Destroy();
}

SPLSandBox::Moc= kGlobal() lets us set global variables (including super
gl= obals) inside the sandbox.

Now, $aTestResul= ts contains the results of each test, run with separate
$_= GET parameters.

With this structure, you co= uld get **every** valid URL from a database
and run a unit= test with custom $_GET params on your FrontController to
= make sure everything works.

Hey Nick,

This looks quite val= uable, and I assume auto loading would work just like normal? Register a= n autoloader that will eventually require the file and call this functio= n?

It would be nice to provide a simplified= api as well, maybe =E2=80=9CCopyCurrentEnvironment()=E2=80=9D or someth= ing?  In most cases, it is easier/faster to find things to remove v= s. adding everything on every plugin/request every time. 
=

In saying that, it would be great if there was an ap= i for =E2=80=9Csharing=E2=80=9D a base-sandbox pool via shm (or similar = to a pool) so that the base vm doesn=E2=80=99t need to be recreated pote= ntially hundreds of times per request. 

=E2=80=94 Rob
--a24101511b5145478470261418c83560--