Skip to content

Commit 7102b95

Browse files
[5.x] Block methods in Antlers by default (#14059)
Co-authored-by: John Koster <john@stillat.com>
1 parent 617ce53 commit 7102b95

10 files changed

Lines changed: 229 additions & 29 deletions

File tree

src/Providers/ViewServiceProvider.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ private function registerAntlers()
103103
$runtimeConfig->guardedContentTagPatterns = config('statamic.antlers.guardedContentTags', []);
104104
$runtimeConfig->guardedContentModifiers = config('statamic.antlers.guardedContentModifiers', []);
105105
$runtimeConfig->allowPhpInUserContent = config('statamic.antlers.allowPhpInContent', false);
106+
$runtimeConfig->allowMethodsInUserContent = config('statamic.antlers.allowMethodsInContent', false);
106107

107108
$runtimeConfig->guardedContentVariablePatterns = array_merge(
108109
$runtimeConfig->guardedVariablePatterns,

src/View/Antlers/Language/Errors/AntlersErrorCodes.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,5 @@ class AntlersErrorCodes
131131
const TYPE_UNPAIRED_CLOSING_TAG = 'ANTLR_131';
132132
const TYPE_UNEXPECTED_CHARACTER_WHILE_PARSING_SHORTHAND_PARAMETER = 'ANTLR_132';
133133
const TYPE_UNEXPECTED_EOI_PARSING_SHORTHAND_PARAMETER = 'ANTLR_133';
134+
const RUNTIME_METHOD_CALL_USER_CONTENT = 'ANTLR_135';
134135
}

src/View/Antlers/Language/Runtime/GlobalRuntimeState.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,13 @@ public static function mergeTagRuntimeAssignments($assignments)
184184
*/
185185
public static $allowPhpInContent = false;
186186

187+
/**
188+
* Controls if method invocations are evaluated in user content.
189+
*
190+
* @var bool
191+
*/
192+
public static $allowMethodsInContent = false;
193+
187194
/**
188195
* Maintains a list of all field prefixes that have been encountered.
189196
*
@@ -226,6 +233,9 @@ public static function resetGlobalState()
226233
self::$yieldCount = 0;
227234
self::$yieldStacks = [];
228235
self::$abandonedNodes = [];
236+
self::$isEvaluatingUserData = false;
237+
self::$isEvaluatingData = false;
238+
self::$userContentEvalState = null;
229239

230240
StackReplacementManager::clearStackState();
231241
LiteralReplacementManager::resetLiteralState();

src/View/Antlers/Language/Runtime/NodeProcessor.php

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2166,21 +2166,27 @@ public function reduce($processNodes)
21662166

21672167
if ($val instanceof Value) {
21682168
if ($val->shouldParseAntlers()) {
2169+
$prevIsEvaluatingUserData = GlobalRuntimeState::$isEvaluatingUserData;
2170+
$prevIsEvaluatingData = GlobalRuntimeState::$isEvaluatingData;
21692171
GlobalRuntimeState::$isEvaluatingUserData = true;
21702172
GlobalRuntimeState::$isEvaluatingData = true;
21712173
GlobalRuntimeState::$userContentEvalState = [
21722174
$val,
21732175
$node,
21742176
];
21752177

2176-
$val = $val->antlersValue($this->antlersParser, $this->getActiveData());
2177-
GlobalRuntimeState::$userContentEvalState = null;
2178-
GlobalRuntimeState::$isEvaluatingUserData = false;
2179-
GlobalRuntimeState::$isEvaluatingData = false;
2178+
try {
2179+
$val = $val->antlersValue($this->antlersParser, $this->getActiveData());
2180+
} finally {
2181+
GlobalRuntimeState::$userContentEvalState = null;
2182+
GlobalRuntimeState::$isEvaluatingUserData = $prevIsEvaluatingUserData;
2183+
GlobalRuntimeState::$isEvaluatingData = $prevIsEvaluatingData;
2184+
}
21802185
} else {
2186+
$prevIsEvaluatingData = GlobalRuntimeState::$isEvaluatingData;
21812187
GlobalRuntimeState::$isEvaluatingData = true;
21822188
$val = $val->value();
2183-
GlobalRuntimeState::$isEvaluatingData = false;
2189+
GlobalRuntimeState::$isEvaluatingData = $prevIsEvaluatingData;
21842190
}
21852191
}
21862192

@@ -2630,9 +2636,13 @@ protected function runModifier($modifier, $parameters, $data, $context = [])
26302636
return $data->value();
26312637
}
26322638

2639+
$prevIsEvaluatingUserData = GlobalRuntimeState::$isEvaluatingUserData;
26332640
GlobalRuntimeState::$isEvaluatingUserData = true;
2634-
$value = $data->antlersValue($this->antlersParser, $context);
2635-
GlobalRuntimeState::$isEvaluatingUserData = false;
2641+
try {
2642+
$value = $data->antlersValue($this->antlersParser, $context);
2643+
} finally {
2644+
GlobalRuntimeState::$isEvaluatingUserData = $prevIsEvaluatingUserData;
2645+
}
26362646

26372647
try {
26382648
return Modify::value($value)->context($context)->$modifier($parameters)->fetch();

src/View/Antlers/Language/Runtime/PathDataManager.php

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -983,12 +983,14 @@ public static function reduce($value, $isPair = true, $reduceBuildersAndAugmenta
983983
$reductionValue = array_pop($reductionStack);
984984

985985
if ($reductionValue instanceof Value) {
986+
$prevIsEvaluatingUserData = GlobalRuntimeState::$isEvaluatingUserData;
987+
$prevIsEvaluatingData = GlobalRuntimeState::$isEvaluatingData;
986988
GlobalRuntimeState::$isEvaluatingUserData = true;
987989
GlobalRuntimeState::$isEvaluatingData = true;
988990
$augmented = RuntimeValues::getValue($reductionValue);
989991
$augmented = self::guardRuntimeReturnValue($augmented);
990-
GlobalRuntimeState::$isEvaluatingUserData = false;
991-
GlobalRuntimeState::$isEvaluatingData = false;
992+
GlobalRuntimeState::$isEvaluatingUserData = $prevIsEvaluatingUserData;
993+
GlobalRuntimeState::$isEvaluatingData = $prevIsEvaluatingData;
992994

993995
if (! $isPair) {
994996
return $augmented;
@@ -998,45 +1000,52 @@ public static function reduce($value, $isPair = true, $reduceBuildersAndAugmenta
9981000

9991001
continue;
10001002
} elseif ($reductionValue instanceof Values) {
1003+
$prevIsEvaluatingData = GlobalRuntimeState::$isEvaluatingData;
10011004
GlobalRuntimeState::$isEvaluatingData = true;
10021005
$reductionStack[] = $reductionValue->toArray();
1003-
GlobalRuntimeState::$isEvaluatingData = false;
1006+
GlobalRuntimeState::$isEvaluatingData = $prevIsEvaluatingData;
10041007

10051008
continue;
10061009
} elseif ($reductionValue instanceof \Statamic\Entries\Collection) {
1010+
$prevIsEvaluatingData = GlobalRuntimeState::$isEvaluatingData;
10071011
GlobalRuntimeState::$isEvaluatingData = true;
10081012
$reductionStack[] = RuntimeValues::resolveWithRuntimeIsolation($reductionValue);
1009-
GlobalRuntimeState::$isEvaluatingData = false;
1013+
GlobalRuntimeState::$isEvaluatingData = $prevIsEvaluatingData;
10101014

10111015
continue;
10121016
} elseif ($reductionValue instanceof ArrayableString) {
1017+
$prevIsEvaluatingData = GlobalRuntimeState::$isEvaluatingData;
10131018
GlobalRuntimeState::$isEvaluatingData = true;
10141019
$reductionStack[] = $reductionValue->toArray();
1015-
GlobalRuntimeState::$isEvaluatingData = false;
1020+
GlobalRuntimeState::$isEvaluatingData = $prevIsEvaluatingData;
10161021

10171022
continue;
10181023
} elseif ($reductionValue instanceof Augmentable) {
10191024
// Avoids resolving augmented data "too early".
10201025
if ($reduceBuildersAndAugmentables) {
1026+
$prevIsEvaluatingUserData = GlobalRuntimeState::$isEvaluatingUserData;
1027+
$prevIsEvaluatingData = GlobalRuntimeState::$isEvaluatingData;
10211028
GlobalRuntimeState::$isEvaluatingUserData = true;
10221029
GlobalRuntimeState::$isEvaluatingData = true;
10231030
$augmented = RuntimeValues::resolveWithRuntimeIsolation($reductionValue);
10241031
$augmented = self::guardRuntimeReturnValue($augmented);
1025-
GlobalRuntimeState::$isEvaluatingUserData = false;
1026-
GlobalRuntimeState::$isEvaluatingData = false;
1032+
GlobalRuntimeState::$isEvaluatingUserData = $prevIsEvaluatingUserData;
1033+
GlobalRuntimeState::$isEvaluatingData = $prevIsEvaluatingData;
10271034
$reductionStack[] = $augmented;
10281035
} else {
10291036
return $reductionValue;
10301037
}
10311038

10321039
continue;
10331040
} elseif ($reductionValue instanceof Collection) {
1041+
$prevIsEvaluatingData = GlobalRuntimeState::$isEvaluatingData;
10341042
GlobalRuntimeState::$isEvaluatingData = true;
10351043
$reductionStack[] = $reductionValue->all();
1036-
GlobalRuntimeState::$isEvaluatingData = false;
1044+
GlobalRuntimeState::$isEvaluatingData = $prevIsEvaluatingData;
10371045

10381046
continue;
10391047
} elseif ($reductionValue instanceof Model) {
1048+
$prevIsEvaluatingData = GlobalRuntimeState::$isEvaluatingData;
10401049
GlobalRuntimeState::$isEvaluatingData = true;
10411050
$data = $reductionValue->toArray();
10421051

@@ -1053,19 +1062,21 @@ public static function reduce($value, $isPair = true, $reduceBuildersAndAugmenta
10531062
}
10541063

10551064
$reductionStack[] = $data;
1056-
GlobalRuntimeState::$isEvaluatingData = false;
1065+
GlobalRuntimeState::$isEvaluatingData = $prevIsEvaluatingData;
10571066

10581067
continue;
10591068
} elseif ($reductionValue instanceof Arrayable) {
1069+
$prevIsEvaluatingData = GlobalRuntimeState::$isEvaluatingData;
10601070
GlobalRuntimeState::$isEvaluatingData = true;
10611071
$reductionStack[] = $reductionValue->toArray();
1062-
GlobalRuntimeState::$isEvaluatingData = false;
1072+
GlobalRuntimeState::$isEvaluatingData = $prevIsEvaluatingData;
10631073

10641074
continue;
10651075
} elseif ($reductionValue instanceof Builder && $reduceBuildersAndAugmentables) {
1076+
$prevIsEvaluatingData = GlobalRuntimeState::$isEvaluatingData;
10661077
GlobalRuntimeState::$isEvaluatingData = true;
10671078
$reductionStack[] = $reductionValue->get();
1068-
GlobalRuntimeState::$isEvaluatingData = false;
1079+
GlobalRuntimeState::$isEvaluatingData = $prevIsEvaluatingData;
10691080

10701081
continue;
10711082
}
@@ -1087,6 +1098,8 @@ public static function reduce($value, $isPair = true, $reduceBuildersAndAugmenta
10871098
*/
10881099
public static function reduceForAntlers($value, Parser $parser, $data, $isPair = true)
10891100
{
1101+
$prevIsEvaluatingUserData = GlobalRuntimeState::$isEvaluatingUserData;
1102+
$prevIsEvaluatingData = GlobalRuntimeState::$isEvaluatingData;
10901103
GlobalRuntimeState::$isEvaluatingUserData = true;
10911104
GlobalRuntimeState::$isEvaluatingData = true;
10921105

@@ -1099,20 +1112,14 @@ public static function reduceForAntlers($value, Parser $parser, $data, $isPair =
10991112
}
11001113

11011114
if ($value instanceof Value) {
1102-
GlobalRuntimeState::$isEvaluatingUserData = true;
1103-
11041115
if (! $isPair) {
11051116
$returnValue = $value->antlersValue($parser, $data);
11061117
} else {
11071118
$returnValue = self::reduce($value->antlersValue($parser, $data));
11081119
}
11091120
$returnValue = self::guardRuntimeReturnValue($returnValue);
1110-
1111-
GlobalRuntimeState::$isEvaluatingUserData = false;
11121121
} elseif ($value instanceof Values) {
1113-
GlobalRuntimeState::$isEvaluatingUserData = true;
11141122
$returnValue = $value->toArray();
1115-
GlobalRuntimeState::$isEvaluatingUserData = false;
11161123
} else {
11171124
if (! $isPair) {
11181125
if (is_array($value)) {
@@ -1127,8 +1134,8 @@ public static function reduceForAntlers($value, Parser $parser, $data, $isPair =
11271134
}
11281135
}
11291136

1130-
GlobalRuntimeState::$isEvaluatingUserData = false;
1131-
GlobalRuntimeState::$isEvaluatingData = false;
1137+
GlobalRuntimeState::$isEvaluatingUserData = $prevIsEvaluatingUserData;
1138+
GlobalRuntimeState::$isEvaluatingData = $prevIsEvaluatingData;
11321139

11331140
return $returnValue;
11341141
}

src/View/Antlers/Language/Runtime/RuntimeConfiguration.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,16 @@ class RuntimeConfiguration
109109
*/
110110
public $allowPhpInUserContent = false;
111111

112+
/**
113+
* Indicates if method invocations should be evaluated in user content.
114+
*
115+
* When disabled, method calls like object:method() within
116+
* fields with antlers:true will be blocked.
117+
*
118+
* @var bool
119+
*/
120+
public $allowMethodsInUserContent = false;
121+
112122
/**
113123
* Registers a new Antlers preparser callback.
114124
*

src/View/Antlers/Language/Runtime/RuntimeParser.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ public function __construct(DocumentParser $documentParser, NodeProcessor $nodeP
134134
public function setRuntimeConfiguration(RuntimeConfiguration $configuration)
135135
{
136136
GlobalRuntimeState::$allowPhpInContent = $configuration->allowPhpInUserContent;
137+
GlobalRuntimeState::$allowMethodsInContent = $configuration->allowMethodsInUserContent;
137138
GlobalRuntimeState::$throwErrorOnAccessViolation = $configuration->throwErrorOnAccessViolation;
138139
GlobalRuntimeState::$bannedVarPaths = $configuration->guardedVariablePatterns;
139140
GlobalRuntimeState::$bannedContentVarPaths = $configuration->guardedContentVariablePatterns;

src/View/Antlers/Language/Runtime/Sandbox/Environment.php

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Exception;
77
use Illuminate\Support\Arr;
88
use Illuminate\Support\Collection;
9+
use Illuminate\Support\Facades\Log;
910
use Illuminate\Support\MessageBag;
1011
use Illuminate\Support\ViewErrorBag;
1112
use Statamic\Contracts\Query\Builder;
@@ -890,6 +891,27 @@ public function process($nodes)
890891

891892
continue;
892893
} elseif ($currentNode instanceof MethodInvocationNode) {
894+
if (GlobalRuntimeState::$isEvaluatingUserData && ! GlobalRuntimeState::$allowMethodsInContent) {
895+
array_pop($stack);
896+
897+
if (GlobalRuntimeState::$throwErrorOnAccessViolation) {
898+
throw ErrorFactory::makeRuntimeError(
899+
AntlersErrorCodes::RUNTIME_METHOD_CALL_USER_CONTENT,
900+
$currentNode,
901+
'Method invocation in user content.'
902+
);
903+
} else {
904+
Log::warning('Method call evaluated in user content.', [
905+
'file' => GlobalRuntimeState::$currentExecutionFile,
906+
'trace' => GlobalRuntimeState::$templateFileStack,
907+
]);
908+
}
909+
910+
$stack[] = null;
911+
912+
continue;
913+
}
914+
893915
$leftNode = array_pop($stack);
894916

895917
if ($leftNode == null) {
@@ -1369,22 +1391,26 @@ private function adjustValue($value, $originalNode)
13691391
private function checkForFieldValue($value, $hasModifiers = false, $modifierChain = null)
13701392
{
13711393
if ($value instanceof Value) {
1394+
$prevIsEvaluatingUserData = GlobalRuntimeState::$isEvaluatingUserData;
13721395
GlobalRuntimeState::$isEvaluatingUserData = true;
13731396
if ($value->shouldParseAntlers()) {
13741397
if (! $hasModifiers || ($modifierChain != null && $modifierChain[0]->nameNode->name != 'raw')) {
13751398
GlobalRuntimeState::$userContentEvalState = [
13761399
$value,
13771400
$this->nodeProcessor->getActiveNode(),
13781401
];
1379-
$value = $value->antlersValue($this->nodeProcessor->getAntlersParser(), $this->data);
1380-
GlobalRuntimeState::$userContentEvalState = null;
1402+
try {
1403+
$value = $value->antlersValue($this->nodeProcessor->getAntlersParser(), $this->data);
1404+
} finally {
1405+
GlobalRuntimeState::$userContentEvalState = null;
1406+
}
13811407
}
13821408
} else {
13831409
if (! $hasModifiers) {
13841410
$value = $value->value();
13851411
}
13861412
}
1387-
GlobalRuntimeState::$isEvaluatingUserData = false;
1413+
GlobalRuntimeState::$isEvaluatingUserData = $prevIsEvaluatingUserData;
13881414
}
13891415

13901416
return $value;

tests/Antlers/ParserTestCase.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ protected function setUp(): void
4646
parent::setUp();
4747

4848
GlobalRuntimeState::resetGlobalState();
49+
GlobalRuntimeState::$throwErrorOnAccessViolation = false;
50+
GlobalRuntimeState::$allowPhpInContent = false;
51+
GlobalRuntimeState::$allowMethodsInContent = false;
4952

5053
$this->setupTestBlueprintAndFields();
5154

0 commit comments

Comments
 (0)