Newsgroups: php.internals Path: news.php.net Xref: news.php.net php.internals:124819 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 A63951A00B7 for ; Wed, 7 Aug 2024 09:55:15 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=php.net; s=mail; t=1723024617; bh=qhuKNfxQagUMXv/bj9a3hwKumCCOV8fpulIOtMkvFi8=; h=Subject:From:To:Date:In-Reply-To:References:From; b=lO/NZP2v7NvKYGPQf1YVD1fkHo2EYY5L14BIme2q+jR0VdwOdY0CP62aklX0CawFx ZLXJRzTEbwMKVm4/CMqCIpy5CNz/DDeVF0U9bJ/UTw4Z85chsNQayPOg3u/rY8qfe6 c2QJFazpGcwYxyPmAWKnS9i6KqIHaUllyCeUgNI0U36Lv1nfkTq/fdrSMkLycsisLm n7hm5/M+NLn5Om+KuvIpFY6MyloFo5q9fhxd/gg7YMzG/JV7Kvss1R468fyHQZpWRW pypKQC03AZbnqA/01x+NCqz2iawTW1/9+YqAFBWxu8owtEma92d7ODjseQVM1qWMsl AW2Cc3+Ywu8aQ== Received: from php-smtp4.php.net (localhost [127.0.0.1]) by php-smtp4.php.net (Postfix) with ESMTP id 985CF180069 for ; Wed, 7 Aug 2024 09:56:56 +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.6 required=5.0 tests=BAYES_50,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,DMARC_PASS,SPF_HELO_PASS, SPF_PASS autolearn=no autolearn_force=no version=4.0.0 X-Spam-Virus: No X-Envelope-From: Received: from ageofdream.com (ageofdream.com [45.33.21.21]) (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 ; Wed, 7 Aug 2024 09:56:56 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ageofdream.com; s=ageofdream; t=1723024512; bh=qhuKNfxQagUMXv/bj9a3hwKumCCOV8fpulIOtMkvFi8=; h=Subject:From:To:Date:In-Reply-To:References:From; b=nUAekw7wrIiHwZBq+oOTIKBD8Fb5Qzbo2X/4EGyNr4WyKyNJ+d+9kV5upmqcdHgFs YxHQb2M0kalnBqBYEz3qX0p5mF0ASBXyMNvp9ZUH98co46BjD9/PUX2EpC/Bpkwwm6 fUHi1r4+Y7MdFyNRmCzGsqjqTrEWv4Yhh/o/pebtA3K24fKFgczSpvU5ZHvEBbN2GS JgseQGhi0DsL9hwuLLT4I8TZFgPYlABz2zGVGCBQeftGrq4ZGYgGaARiBZTRWvLtrT W481cRYaYFb6Y4BGMIb0l1IUcrthAwdsCad3Z4EPyG+cuGTh/pDsPFO6xM5omP0eFT dYlxw+ZTAExAg== Received: from [192.168.1.7] (unknown [72.255.193.122]) by ageofdream.com (Postfix) with ESMTPSA id 21B7F25086 for ; Wed, 7 Aug 2024 05:55:12 -0400 (EDT) Message-ID: <73c2a78bec2a65cdff2cee0745305b1ea72f2193.camel@ageofdream.com> Subject: Re: [PHP-DEV] [Discussion] Sandbox API To: internals@lists.php.net Date: Wed, 07 Aug 2024 05:55:11 -0400 In-Reply-To: <662030fe-b87f-4afd-94a3-6e9210bc785b@app.fastmail.com> References: <32236410-e4ad-4c63-b7e7-cdbd3832bbc1@app.fastmail.com> <70ea158f8c37b9d3acfb7a6ab0f2e76adfb63cdd.camel@ageofdream.com> <662030fe-b87f-4afd-94a3-6e9210bc785b@app.fastmail.com> Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable User-Agent: Evolution 3.46.4-2 Precedence: bulk list-help: list-post: List-Id: internals.lists.php.net x-ms-reactions: disallow MIME-Version: 1.0 From: lists@ageofdream.com (Nick Lockheart) On Tue, 2024-08-06 at 20:51 +0200, Rob Landers wrote: > Hey Nick, >=20 > Looking forward to the RFC! >=20 > On Tue, Aug 6, 2024, at 19:28, Nick Lockheart wrote: > > > =C2=A0 > > > This looks quite valuable, and I assume auto loading would work > > > just > > > like normal? Register an autoloader that will eventually require > > > the > > > file and call this function? > > > =C2=A0 > > > It would be nice to provide a simplified api as well, maybe > > > =E2=80=9CCopyCurrentEnvironment()=E2=80=9D or something? =C2=A0In mos= t cases, it is > > > easier/faster to find things to remove vs. adding everything on > > > every > > > plugin/request every time.=C2=A0 > > > =C2=A0 > > > In saying that, it would be great if there was an api 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 potentially hundreds of times pe= r > > > request.=C2=A0 > > > =C2=A0 > >=20 > > I didn't want to be too overwhelming on the first post, but since > > it > > seems the feedback is positive, here's a more complete list of what > > I > > think should be included: > >=20 > >=20 > > // Passthroughs: > >=20 > > // Make all user and built-in global functions > > // available inside the sandbox: > > SPLSandBox::PassGlobalFunctions(); >=20 > Bike shed: maybe have PassGlobals() instead? Or rather, PassNamespace > and have \ be a valid namespace.=C2=A0 >=20 That's not a bad suggestion, but I was concerned that there isn't a way to grab everything in a namespace. I suppose if the class is already registered then a "pass namespace" would work for everything registered at the time of the call, but wouldn't work with global auto load after the call. > >=20 > > // Make all built-in (but not user) functions > > // available inside the sandbox: > > SPLSandBox::PassBuiltInFunctions(); > >=20 > >=20 > > // Make all built-in (but not user) functions > > // available inside the sandbox, EXCEPT blacklisted functions: > > SPLSandBox::PassBuiltInFunctionsExcept(['eval','exit']); > >=20 > > (assuming exit becomes a function). >=20 > I feel like exit (function or not) should just return from the > sandbox and shouldn=E2=80=99t be disable-able. For example, a plugin migh= t > detect a valid etag and set headers to 302 and exit.=C2=A0 >=20 > >=20 > >=20 > > // Allow only specific functions to be called (whitelist method): > > $aWhiteList =3D ['array_key_exists','in_array']; > > SPLSandBox::PassFunctions($aWhiteList); >=20 > Might be a good idea to combine these two? Allow passing a whitelist > AND a blacklist? Are these supposed to be static or on an instance of > a sandbox? >=20 I'm using static notation, but the intent is that they are methods called on an instance. Re: combining, it's more of a style preference. I like naming things: PassBuiltInFunctionsExcept() for example, because you can read the code without comments and understand what it is doing, without documentation. Any time you can name things so the code reads like a sentence is a win in my book. > >=20 > > // Allow specific classes to be used by sandbox code: > > $aClassList =3D ['\MyAPP\PluginAPI']; > > SPLSandBox::PassClasses($aClassList); > >=20 > >=20 > > // Allow specific constants to be seen by sandbox code: > > SPLSandBox::PassConstants(['\DB_USERNAME','\DB_PASSWORD']); > >=20 > >=20 > >=20 > > // Language Construct Callbacks: > >=20 > > The callbacks allow the outer code to control and monitor certain > > language features of the sandboxed code during execution. > >=20 > > // Called when the sandbox code tries to include or require > > something: > > SPLSandBox::RegisterIncludeHandler(); >=20 > Does including a file from outside the sandbox (next call) call this > handler? >=20 No, the callback monitors what the sandbox code is trying to do and can allow, deny, or modify it. > >=20 > > // Includes a file into the sandbox: > > SPLSandBox::Include('path/to/file.php'); > >=20 > > // Your sandbox autoloader logic could be incorporated here: > > SPLSandBox::RegisterAutoLoadHandler(); > >=20 > >=20 > > // But, for unit testing with mocks and stubs, > > // it might be better to use: > > SPLSandBox::RegisterNewHandler(); > >=20 > > The NewHandler callback is called every time sandboxed code tries > > to > > instantiate an object with `new`. >=20 > Why not use the current autoload logic? For unit testing purposes, you can intercept calls to `new` from the code under test and return a mock object. For example, if your class assembles SQL queries and sends them to a PDO object, you can give the SQL class a dummy PDO to test that it's building queries correctly, without needing a database available in your build environment while the test is being run. This tool is optional, like most of the callbacks, so you just implement the ones you need. If you don't define a NewHandler, then the sandbox code runs the normal `new` behavior, with or without a sandbox auto loader. >=20 > > // Example: Override what `new` returns to code running in the > > sandbox: > > function MyNewHandler(string $ClassName, array $aConstructorArgs){ > >=20 > > =C2=A0=C2=A0 if($ClassName =3D=3D=3D '\DateTime'){ > > =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 return new FakeDate(); > > =C2=A0=C2=A0 } > > =C2=A0=C2=A0 return new $ClassName($aConstructorArgs); > > } > >=20 > >=20 > > // Every time a sandboxed class calls a method, call this first: > > SPLSandBox::RegisterMethodCallHandler(); > >=20 > > Useful for unit testing to monitor if the tested class is calling > > the > > methods it should be calling. Ignores visibility rules. > >=20 > > Could also allow for infinite recursion detection from the outside. >=20 > I think this is handled automatically now.=C2=A0 >=20 > >=20 > >=20 > > // The companion for static method calls, gets called > > // every time a method is called on a class statically: > > SPLSandBox::RegisterStaticMethodCallHandler(); > >=20 > >=20 > >=20 > > // Each time a sandboxed loop iterates, call this first: > > // Allows the outer code to put limit breaks on the sandboxed code. > > SPLSandBox::RegisterLoopHandler(); > >=20 > > The callback takes the type of loop, and the variables that make up > > the > > loop ($i for for(), $Key =3D> $value for foreach(), etc) > >=20 > >=20 > > // If the sandboxed code calls echo, print, or > > // causes any output to occur (ie outside of > SPLSandBox::RegisterEchoHandler(); > >=20 > > Could be used to make sure templates behave as desired, but perhaps > > even more useful, it lets you *fail* unit tests if any output > > occurs > > from a test that shouldn't produce output. > >=20 > > ie. Catch echo statements used in testing, or whitespace after a > > closing ?> tag. > >=20 > >=20 > > // If the sandbox code tries to use `exit` or `die`, > > // call this function instead: > > SPLSandBox::RegisterExitHandler(); > >=20 > > You'll probably want to destroy the sandbox from the outside (see > > below), rather than letting sandboxed code halt the test framework > > or > > main application. > >=20 > >=20 > > // If sandboxed code throws, it should *not* > > // be a throw in the outer application space. > > // Every exception throw triggers this callback, > > // even if there is a catch block: > > SPLSandBox::RegisterExceptionHandler(); > >=20 > > // When a catch block runs, invoke this callback first: > > SPLSandBox::RegisterCatchHandler(); > >=20 > > Allows unit tests to make sure that exceptions are handled > > correctly. > >=20 > >=20 > > // For non-Exceptions (warning, notice, deprecated, fatal, > > // and yes, maybe even parse because we're in a sandbox): > > SPLSandBox::RegisterErrorHandler(); > >=20 > >=20 > >=20 > > // Inside your Handlers, you may want to know the file and line > > // that triggered the callback.=C2=A0 > > SPLSandBox::GetCurrentLine(); > > SPLSandBox::GetFileName(); > > SPLSandBox::GetClassName(); > > SPLSandBox::GetFunctionName(); > >=20 > > For example, if you want to catch any echo left behind from > > debugging, > > you might also want to find the line and file where the statement > > is > > located to remove it. The above methods would be usable inside any > > of > > the callbacks. > >=20 > >=20 > > // Inside your Handlers, abort execution: > > SPLSandBox::Stop(); >=20 > >=20 > >=20 > > // Resource Limits, hopefully self explanatory: > > SPLSandBox::SetMemoryLimit(); > > SPLSandBox::SetExecutionTimeLimit(); > >=20 > >=20 > > // Mocks, Stubs: > >=20 > > // Put a function from the outer application > > // into the sandbox as the specified name: > > SPLSandBox::MockFunction('\mocks\fopen','\fopen'); > >=20 > >=20 > > // Put a class from the outer application > > // into the sandbox as the specified name: > > SPLSandBox::MockClass('\Mocks\FakeTime','\DateTime'); > >=20 > >=20 > > // Set global variables (and super globals) inside the sandbox: > > SPLSandBox::MockGlobal('$_GET',$aGetVars);=C2=A0 > >=20 > >=20 > > // Set global constants inside the sandbox: > > SPLSandBox::MockConstant('MY_CONSTANT',$Value);=C2=A0 > >=20 > >=20 > >=20 > > // Invocation > >=20 > > // You can run procedural code to setup your test environment. > > // Runs the array of code lines in the sandbox context: > > $aProceduralCode =3D [ > > =C2=A0=C2=A0 "$a =3D 1;", > > =C2=A0=C2=A0 "$b =3D 2;", > > =C2=A0=C2=A0 "$c =3D DoSomething($a, $b);" > > ]; > >=20 > > SPLSandBox::Procedure($aProceduralCode); > >=20 > >=20 > > // You can get a pointer to an object instantiated in the sandbox: > > $oClass =3D SPLSandBox::GetInstance('ClassName'); > >=20 > > // And use it like you normally would: > > $oClass->DoSomething(); > >=20 > > This class runs entirely in the sandbox. > >=20 > >=20 > > // You cleanup the resource with: > > SPLSandBox::Destroy(); > >=20 >=20 > Looks pretty exciting and useful! Since you=E2=80=99re imagining it being= a > part of SPL, why not implement this in its own extension? It looks > like the pecl process is pretty convoluted to get an extension listed > there, but many popular extensions live outside of pecl too. >=20 > =E2=80=94 Rob I think this would need to be a team effort. Some of this requires doing stuff that I'm not sure how to do, for example, I don't know how to make a user land callback function get invoked when `new` is called, that injects a new thing back to the calling code. It seems like this would need to be part of core. On the other hand, this could be the defining feature of whatever PHP version it was included with. A security framework for third party extensions and a first class unit testing framework, would benefit almost everyone using PHP.