feat: VivesPOS landing on Winter CMS 1.2 — theme + plugin + Dockerfile
Some checks are pending
Module sub-split / Sub-split (push) Waiting to run
Some checks are pending
Module sub-split / Sub-split (push) Waiting to run
- Base: wintercms/winter branch 1.2 (full framework) - Theme vivespos: Canvas 7 + Bootstrap 5 CDN, custom CSS - Layout: deferred GTM/GA4 tracking, JSON-LD SoftwareApplication - Partials: hero (offline-first), features, modes (offline/nube toggle), screenshots, pricing (3 planes), comparison, FAQ, CTA - Plugin VivesPOS.Site with ContactForm - Dockerfile: PHP 8.2 Apache, port 80, healthcheck - Added winter/wn-pages, blog, sitemap, seo plugins - Active theme set to vivespos
This commit is contained in:
491
modules/system/twig/SecurityPolicy.php
Normal file
491
modules/system/twig/SecurityPolicy.php
Normal file
@@ -0,0 +1,491 @@
|
||||
<?php
|
||||
|
||||
namespace System\Twig;
|
||||
|
||||
use Cms\Classes\Controller;
|
||||
use Cms\Classes\Theme;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Illuminate\Database\ConnectionResolverInterface;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Database\Eloquent\Model as DbModel;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Database\Query\Builder as QueryBuilder;
|
||||
use Illuminate\Pagination\AbstractCursorPaginator;
|
||||
use Illuminate\Pagination\AbstractPaginator;
|
||||
use Illuminate\Session\SessionManager;
|
||||
use Illuminate\Support\Enumerable;
|
||||
use System\Twig\SecurityPolicy\SafeCollection;
|
||||
use System\Twig\SecurityPolicy\SafePaginator;
|
||||
use Twig\Markup;
|
||||
use Twig\Sandbox\SecurityNotAllowedFunctionError;
|
||||
use Twig\Sandbox\SecurityNotAllowedMethodError;
|
||||
use Twig\Sandbox\SecurityNotAllowedPropertyError;
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
use Twig\Template;
|
||||
use Winter\Storm\Halcyon\Builder as HalcyonBuilder;
|
||||
use Winter\Storm\Halcyon\Datasource\DatasourceInterface;
|
||||
use Winter\Storm\Halcyon\Model as HalcyonModel;
|
||||
|
||||
/**
|
||||
* SecurityPolicy globally blocks accessibility of certain methods and properties.
|
||||
*
|
||||
* The policy is a blocklist, but it models the real PHP forwarding behaviour of the
|
||||
* database layer via $blockedForwarders: because `Model::__call` transparently forwards
|
||||
* to the Eloquent Builder, which forwards to the Query Builder, a method blocked on the
|
||||
* Query Builder is also blocked when reached through a Model, Eloquent Builder or Relation.
|
||||
* This is what makes the blocklist complete instead of a game of whack-a-mole.
|
||||
*
|
||||
* @package winter\wn-system-module
|
||||
* @author Alexey Bobkov, Samuel Georges, Luke Towers, Ben Thomson
|
||||
*/
|
||||
final class SecurityPolicy implements SecurityPolicyInterface
|
||||
{
|
||||
/**
|
||||
* @var array<string, string[]> List of forbidden methods, grouped by applicable instance.
|
||||
*/
|
||||
protected $blockedMethods = [
|
||||
'*' => [
|
||||
// Prevent accessing Twig itself
|
||||
'getTwig',
|
||||
|
||||
// Prevent extensions of any objects
|
||||
'addDynamicMethod',
|
||||
'addDynamicProperty',
|
||||
'extendClassWith',
|
||||
'implementClassWith',
|
||||
'getClassExtension',
|
||||
'extendableSet',
|
||||
|
||||
// Prevent directly invoking the magic/extension call machinery
|
||||
'extend',
|
||||
'extendableCall',
|
||||
'extendableCallStatic',
|
||||
'extendableExtendCallback',
|
||||
'extensionExtendCallback',
|
||||
'__call',
|
||||
'__callStatic',
|
||||
'__invoke',
|
||||
|
||||
// Prevent Laravel Macroable injection
|
||||
'macro',
|
||||
'mixin',
|
||||
|
||||
// Prevent binding to, or firing, events
|
||||
'bindEvent',
|
||||
'bindEventOnce',
|
||||
'fireEvent',
|
||||
'fireSystemEvent',
|
||||
],
|
||||
|
||||
// Prevent some controller methods. The controller is a fixed, known object; these
|
||||
// methods run nested page cycles, render arbitrary partials, or read files.
|
||||
Controller::class => [
|
||||
'runPage',
|
||||
'renderPage',
|
||||
'getLoader',
|
||||
'run',
|
||||
'combineAssets',
|
||||
'renderPartial',
|
||||
'renderContent',
|
||||
],
|
||||
|
||||
// Prevent model data modification. Methods that forward to the query layer
|
||||
// (increment, decrement, touch, getConnection, ...) are covered transitively by
|
||||
// $blockedForwarders; only methods that physically live on the Model are listed here.
|
||||
DbModel::class => [
|
||||
'fill',
|
||||
'forceFill',
|
||||
'setAttribute',
|
||||
'setRawAttributes',
|
||||
'save',
|
||||
'saveQuietly',
|
||||
'saveOrFail',
|
||||
'push',
|
||||
'pushQuietly',
|
||||
'update',
|
||||
'updateQuietly',
|
||||
'updateOrFail',
|
||||
'delete',
|
||||
'deleteQuietly',
|
||||
'deleteOrFail',
|
||||
'forceDelete',
|
||||
'destroy',
|
||||
'forceDestroy',
|
||||
'restore',
|
||||
'restoreQuietly',
|
||||
'getQuery',
|
||||
// Re-pointing the table/connection would allow reading arbitrary tables/databases
|
||||
'setTable',
|
||||
'setConnection',
|
||||
'on',
|
||||
'onWriteConnection',
|
||||
'setKeyName',
|
||||
'setKeyType',
|
||||
'setIncrementing',
|
||||
'setPerPage',
|
||||
'setDateFormat',
|
||||
'offsetSet',
|
||||
'offsetUnset',
|
||||
// Disabling mass-assignment / event protection, or executing callbacks
|
||||
'unguard',
|
||||
'reguard',
|
||||
'unguarded',
|
||||
'withoutEvents',
|
||||
'withoutTouching',
|
||||
'withoutTouchingOn',
|
||||
// getConnectionResolver returns the DatabaseManager, whose __call proxies raw SQL
|
||||
'getConnectionResolver',
|
||||
'setConnectionResolver',
|
||||
'unsetConnectionResolver',
|
||||
'flushEventListeners',
|
||||
'getEventDispatcher',
|
||||
'setEventDispatcher',
|
||||
'unsetEventDispatcher',
|
||||
],
|
||||
|
||||
EloquentBuilder::class => [
|
||||
'forceDelete',
|
||||
'create',
|
||||
'createQuietly',
|
||||
'forceCreate',
|
||||
'forceCreateQuietly',
|
||||
'firstOrCreate',
|
||||
'createOrFirst',
|
||||
'updateOrCreate',
|
||||
'incrementOrCreate',
|
||||
'fillAndInsert',
|
||||
'fillAndInsertOrIgnore',
|
||||
'fillAndInsertGetId',
|
||||
'touch',
|
||||
'update',
|
||||
'delete',
|
||||
'upsert',
|
||||
],
|
||||
|
||||
QueryBuilder::class => [
|
||||
'insert',
|
||||
'insertOrIgnore',
|
||||
'insertGetId',
|
||||
'insertUsing',
|
||||
'insertOrIgnoreUsing',
|
||||
'update',
|
||||
'updateFrom',
|
||||
'updateOrInsert',
|
||||
'upsert',
|
||||
'delete',
|
||||
'truncate',
|
||||
'increment',
|
||||
'incrementEach',
|
||||
'incrementQuietly',
|
||||
'decrement',
|
||||
'decrementEach',
|
||||
'decrementQuietly',
|
||||
// Re-pointing the table
|
||||
'from',
|
||||
'fromRaw',
|
||||
'fromSub',
|
||||
// Connection / raw SQL
|
||||
'getConnection',
|
||||
'toRawSql',
|
||||
'selectRaw',
|
||||
'whereRaw',
|
||||
'orWhereRaw',
|
||||
'havingRaw',
|
||||
'orHavingRaw',
|
||||
'orderByRaw',
|
||||
'groupByRaw',
|
||||
'joinSub',
|
||||
'leftJoinSub',
|
||||
'rightJoinSub',
|
||||
'crossJoinSub',
|
||||
'raw',
|
||||
'rawValue',
|
||||
'dd',
|
||||
'dump',
|
||||
'ddRawSql',
|
||||
// callable-typed executors (string callables would execute)
|
||||
'when',
|
||||
'unless',
|
||||
'each',
|
||||
'eachById',
|
||||
'chunk',
|
||||
'chunkById',
|
||||
'chunkByIdDesc',
|
||||
'chunkMap',
|
||||
'tap',
|
||||
'pipe',
|
||||
],
|
||||
|
||||
Relation::class => [
|
||||
'attach',
|
||||
'detach',
|
||||
'sync',
|
||||
'syncWithPivotValues',
|
||||
'syncWithoutDetaching',
|
||||
'toggle',
|
||||
'updateExistingPivot',
|
||||
'save',
|
||||
'saveQuietly',
|
||||
'saveMany',
|
||||
'saveManyQuietly',
|
||||
'create',
|
||||
'createQuietly',
|
||||
'createMany',
|
||||
'createManyQuietly',
|
||||
'forceCreate',
|
||||
'forceCreateQuietly',
|
||||
'push',
|
||||
'update',
|
||||
'updateOrCreate',
|
||||
'firstOrCreate',
|
||||
'firstOrNew',
|
||||
'createOrFirst',
|
||||
'delete',
|
||||
'forceDelete',
|
||||
'associate',
|
||||
'dissociate',
|
||||
'make',
|
||||
'makeMany',
|
||||
],
|
||||
|
||||
HalcyonModel::class => [
|
||||
'fill',
|
||||
'setAttribute',
|
||||
'setRawAttributes',
|
||||
'setSettingsAttribute',
|
||||
'setFileNameAttribute',
|
||||
'save',
|
||||
'push',
|
||||
'update',
|
||||
'delete',
|
||||
'forceDelete',
|
||||
'getQuery',
|
||||
'getDatasource',
|
||||
],
|
||||
|
||||
HalcyonBuilder::class => [
|
||||
'insert',
|
||||
'update',
|
||||
'delete',
|
||||
'forceDelete',
|
||||
'truncate',
|
||||
],
|
||||
|
||||
DatasourceInterface::class => [
|
||||
'insert',
|
||||
'update',
|
||||
'delete',
|
||||
'forceDelete',
|
||||
'write',
|
||||
'usingSource',
|
||||
'pushToSource',
|
||||
'removeFromSource',
|
||||
'select',
|
||||
'selectOne',
|
||||
],
|
||||
|
||||
Theme::class => [
|
||||
'setDirName',
|
||||
'registerHalcyonDatasource',
|
||||
'getDatasource',
|
||||
'writeConfig',
|
||||
'removeCustomData',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string, string> Maps a class to the class its __call forwards to, so the
|
||||
* sandbox enforces the destination's blocklist for a method reached through the source.
|
||||
* The chain is walked transitively (Model -> Eloquent Builder -> Query Builder).
|
||||
*/
|
||||
protected $blockedForwarders = [
|
||||
EloquentBuilder::class => QueryBuilder::class,
|
||||
DbModel::class => EloquentBuilder::class,
|
||||
Relation::class => EloquentBuilder::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string, string[]> List of allowed methods, grouped by applicable instance.
|
||||
* An empty list denies every method on that type (deny-all lock).
|
||||
*/
|
||||
protected $allowedMethods = [
|
||||
SessionManager::class => [
|
||||
'put',
|
||||
'get',
|
||||
'has',
|
||||
'forget',
|
||||
'flush',
|
||||
'pull',
|
||||
],
|
||||
// Locked down entirely: no template legitimately calls raw database or event objects.
|
||||
ConnectionInterface::class => [],
|
||||
ConnectionResolverInterface::class => [],
|
||||
Dispatcher::class => [],
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string, string[]> List of forbidden properties, grouped by applicable instance.
|
||||
*/
|
||||
protected $blockedProperties = [
|
||||
Theme::class => [
|
||||
'datasource',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string[] Twig functions that are not allowed (info-disclosure surface).
|
||||
*/
|
||||
protected $blockedFunctions = [
|
||||
'source',
|
||||
'constant',
|
||||
'enum_cases',
|
||||
];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$properties = [
|
||||
'blockedMethods',
|
||||
'allowedMethods',
|
||||
'blockedProperties',
|
||||
];
|
||||
|
||||
foreach ($properties as $property) {
|
||||
foreach ($this->{$property} as $type => $values) {
|
||||
$this->{$property}[$type] = array_map('strtolower', $values);
|
||||
}
|
||||
}
|
||||
|
||||
$this->blockedFunctions = array_map('strtolower', $this->blockedFunctions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the provided arguments against this security policy
|
||||
*
|
||||
* @param array $tags Array of tags to be checked against the policy ['tag', 'tag2', 'etc']
|
||||
* @param array $filters Array of filters to be checked against the policy ['filter', 'filter2', 'etc']
|
||||
* @param array $functions Array of funtions to be checked against the policy ['function', 'function2', 'etc']
|
||||
* @throws SecurityNotAllowedFunctionError if a given function is not allowed
|
||||
*/
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
{
|
||||
foreach ($functions as $function) {
|
||||
if (in_array(strtolower($function), $this->blockedFunctions)) {
|
||||
throw new SecurityNotAllowedFunctionError(sprintf('Function "%s" is not allowed.', $function), $function);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given property is permitted to be accessed on a given object
|
||||
*
|
||||
* @param object $obj
|
||||
* @param string $property
|
||||
* @throws SecurityNotAllowedPropertyError
|
||||
*/
|
||||
public function checkPropertyAllowed($obj, $property): void
|
||||
{
|
||||
// No need to check Twig internal objects
|
||||
if ($obj instanceof Template || $obj instanceof Markup) {
|
||||
return;
|
||||
}
|
||||
|
||||
$property = strtolower($property);
|
||||
|
||||
foreach ($this->blockedProperties as $type => $properties) {
|
||||
if ($obj instanceof $type && in_array($property, $properties)) {
|
||||
$class = get_class($obj);
|
||||
throw new SecurityNotAllowedPropertyError(sprintf('Getting "%s" property in a "%s" object is blocked.', $property, $class), $class, $property);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given method is allowed to be called on a given object
|
||||
*
|
||||
* @param object $obj
|
||||
* @param string $method
|
||||
* @throws SecurityNotAllowedMethodError
|
||||
*/
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
// No need to check Twig internal objects
|
||||
if ($obj instanceof Template || $obj instanceof Markup) {
|
||||
return;
|
||||
}
|
||||
|
||||
$method = strtolower($method);
|
||||
|
||||
if (in_array($method, $this->blockedMethods['*'])) {
|
||||
$this->throwMethodError($obj, $method);
|
||||
}
|
||||
|
||||
foreach ($this->allowedMethods as $type => $methods) {
|
||||
if ($obj instanceof $type && !in_array($method, $methods)) {
|
||||
$this->throwMethodError($obj, $method);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->blockedMethods as $type => $methods) {
|
||||
if ($type === '*') {
|
||||
continue;
|
||||
}
|
||||
if ($obj instanceof $type && in_array($method, $methods)) {
|
||||
$this->throwMethodError($obj, $method);
|
||||
}
|
||||
}
|
||||
|
||||
// Enforce the blocklists of any class this object's __call forwards to, transitively.
|
||||
// This closes the forwarding escape (e.g. `model.increment()` reaching the Query Builder).
|
||||
foreach ($this->blockedForwarders as $sourceClass => $targetClass) {
|
||||
if (!($obj instanceof $sourceClass)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$cursor = $targetClass;
|
||||
$seen = [];
|
||||
while ($cursor !== null && !isset($seen[$cursor])) {
|
||||
$seen[$cursor] = true;
|
||||
if (in_array($method, $this->blockedMethods[$cursor] ?? [])) {
|
||||
$this->throwMethodError($obj, $method);
|
||||
}
|
||||
$cursor = $this->blockedForwarders[$cursor] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Casts an object to a sandbox-safe proxy before a method is called on it in a template.
|
||||
* Used by the custom GetAttrNode to neutralise callable-passthrough on collections and
|
||||
* paginators (their higher-order methods would otherwise execute arbitrary callables).
|
||||
*
|
||||
* @param mixed $object
|
||||
* @return mixed
|
||||
*/
|
||||
public function castMethodObjectToSafeObject($object)
|
||||
{
|
||||
if ($object instanceof Enumerable) {
|
||||
return new SafeCollection($object);
|
||||
}
|
||||
|
||||
if ($object instanceof AbstractPaginator || $object instanceof AbstractCursorPaginator) {
|
||||
return new SafePaginator($object);
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $obj
|
||||
* @param string $method
|
||||
* @throws SecurityNotAllowedMethodError
|
||||
*/
|
||||
protected function throwMethodError($obj, $method): void
|
||||
{
|
||||
$class = get_class($obj);
|
||||
throw new SecurityNotAllowedMethodError(sprintf('Calling "%s" method on a "%s" object is blocked.', $method, $class), $class, $method);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user