Security context
Medium· 4.3GHSA-vr95-p7q6-8m9q CWE-79Published May 15, 2024

Laravel Cross-site Scripting (XSS) vulnerability in blade templating

Research this vulnerability

Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.

Affected versions

7.0.0 → fixed in 7.1.2

Details

Laravel 7.1.2 addresses a possible XSS related attack vector in the Laravel 7.x Blade Component tag attributes when users are allowed to dictate the value of attributes. All Laravel 7.x users are encouraged to upgrade as soon as possible.

The fix

escape bound attributes that are strings

Taylor Otwell· Mar 12, 2020, 09:06 PM+5310f7263abfc5
src/Illuminate/View/Compilers/ComponentTagCompiler.php+19 4
@@ -23,6 +23,13 @@ class ComponentTagCompiler
*/
protected $aliases = [];
+ /**
+ * The "bind:" attributes that have been compiled.
+ *
+ * @var array
+ */
+ protected $boundAttributes = [];
+
/**
* Create new component tag compiler.
*
@@ -42,6 +49,8 @@ public function __construct(array $aliases = [])
*/
public function compile(string $value)
{
+ $this->boundAttributes = [];
+
$value = $this->compileSlots($value);
return $this->compileTags($value);
@@ -173,7 +182,7 @@ protected function componentString(string $component, array $attributes)
$parameters = $data->all();
}
- return " @component('{$class}', [".$this->attributesToString($parameters).'])
+ return " @component('{$class}', [".$this->attributesToString($parameters, $escapeBound = false).'])
<?php $component->withAttributes(['.$this->attributesToString($attributes->all()).']); ?>';
}
@@ -319,6 +328,7 @@ protected function getAttributesFromAttributeString(string $attributeString)
if (Str::startsWith($attribute, 'bind:')) {
$attribute = Str::after($attribute, 'bind:');
+ $this->boundAttributes[$attribute] = true;
} else {
$value = "'".str_replace("'", "\\'", $value)."'";
}
@@ -349,13 +359,18 @@ protected function parseBindAttributes(string $attributeString)
* Convert an array of attributes to a string.
*
* @param array $attributes
+ * @param bool $escapeBound
* @return string
*/
- protected function attributesToString(array $attributes)
+ protected function attributesToString(array $attributes, $escapeBound = true)
{
return collect($attributes)
- ->map(function (string $value, string $attribute) {
- return "'{$attribute}' => {$value}";
+ ->map(function (string $value, string $attribute) use ($escapeBound) {
+ $attributeVariable = '$attribute_'.md5($attribute);
+
+ return $escapeBound && isset($this->boundAttributes[$attribute]) && $value !== 'true' && ! is_numeric($value)
+ ? "'{$attribute}' => is_string(with({$attributeVariable} = {$value})) ? e({$attributeVariable}) : {$attributeVariable}"
+ : "'{$attribute}' => {$value}";
})
->implode(',');
}
tests/View/Blade/BladeComponentTagCompilerTest.php+16 0
@@ -52,6 +52,22 @@ public function testDataCamelCasing()
<?php \$component->withAttributes([]); ?> @endcomponentClass", trim($result));
}
+ public function testColonData()
+ {
+ $result = (new ComponentTagCompiler(['profile' => TestProfileComponent::class]))->compileTags('<x-profile :user-id="1"></x-profile>');
+
+ $this->assertSame("@component('Illuminate\Tests\View\Blade\TestProfileComponent', ['userId' => 1])
+<?php \$component->withAttributes([]); ?> @endcomponentClass", trim($result));
+ }
+
+ public function testColonAttributesIsEscapedIfStrings()
+ {
+ $result = (new ComponentTagCompiler(['profile' => TestProfileComponent::class]))->compileTags('<x-profile :src="\'foo\'"></x-profile>');
+
+ $this->assertSame("@component('Illuminate\Tests\View\Blade\TestProfileComponent', [])
+<?php \$component->withAttributes(['src' => is_string(with(\$attribute_25d902c24283ab8cfbac54dfa101ad31 = 'foo')) ? e(\$attribute_25d902c24283ab8cfbac54dfa101ad31) : \$attribute_25d902c24283ab8cfbac54dfa101ad31]); ?> @endcomponentClass", trim($result));
+ }
+
public function testColonNestedComponentParsing()
{
$result = (new ComponentTagCompiler(['foo:alert' => TestAlertComponent::class]))->compileTags('<x-foo:alert></x-foo:alert>');
src/Illuminate/View/Compilers/ComponentTagCompiler.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
src/Illuminate/View/Compilers/ComponentTagCompiler.php+1 1
@@ -174,7 +174,7 @@ protected function componentString(string $component, array $attributes)
if (! class_exists($class)) {
$parameters = [
'view' => "'$class'",
- 'data' => '['.$this->attributesToString($data->all()).']',
+ 'data' => '['.$this->attributesToString($data->all(), $escapeBound = false).']',
];
$class = AnonymousComponent::class;
.../View/Compilers/ComponentTagCompiler.php | 2 +-
.../View/Compilers/Concerns/CompilesComponents.php | 14 ++++++++++++++
tests/View/Blade/BladeComponentTagCompilerTest.php | 2 +-
3 files changed, 16 insertions(+), 2 deletions(-)
src/Illuminate/View/Compilers/ComponentTagCompiler.php+1 1
@@ -369,7 +369,7 @@ protected function attributesToString(array $attributes, $escapeBound = true)
$attributeVariable = '$attribute_'.md5($attribute);
return $escapeBound && isset($this->boundAttributes[$attribute]) && $value !== 'true' && ! is_numeric($value)
- ? "'{$attribute}' => is_string(with({$attributeVariable} = {$value})) ? e({$attributeVariable}) : {$attributeVariable}"
+ ? "'{$attribute}' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute({$value})"
: "'{$attribute}' => {$value}";
})
->implode(',');
src/Illuminate/View/Compilers/Concerns/CompilesComponents.php+14 0
@@ -154,4 +154,18 @@ protected function compileProps($expression)
} ?>
<?php unset(\$__defined_vars); ?>";
}
+
+ /**
+ * Sanitize the given component attribute value.
+ *
+ * @param mixed $value
+ * @return mixed
+ */
+ public static function sanitizeComponentAttribute($value)
+ {
+ return is_string($value) ||
+ (is_object($value) && method_exists($value, '__toString', $value))
+ ? e($value)
+ : $value;
+ }
}
tests/View/Blade/BladeComponentTagCompilerTest.php+1 1
@@ -65,7 +65,7 @@ public function testColonAttributesIsEscapedIfStrings()
$result = (new ComponentTagCompiler(['profile' => TestProfileComponent::class]))->compileTags('<x-profile :src="\'foo\'"></x-profile>');
$this->assertSame("@component('Illuminate\Tests\View\Blade\TestProfileComponent', [])
-<?php \$component->withAttributes(['src' => is_string(with(\$attribute_25d902c24283ab8cfbac54dfa101ad31 = 'foo')) ? e(\$attribute_25d902c24283ab8cfbac54dfa101ad31) : \$attribute_25d902c24283ab8cfbac54dfa101ad31]); ?> @endcomponentClass", trim($result));
+<?php \$component->withAttributes(['src' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute('foo')]); ?> @endcomponentClass", trim($result));
}
public function testColonNestedComponentParsing()
src/Illuminate/View/Compilers/ComponentTagCompiler.php | 2 --
1 file changed, 2 deletions(-)
src/Illuminate/View/Compilers/ComponentTagCompiler.php+0 2
@@ -366,8 +366,6 @@ protected function attributesToString(array $attributes, $escapeBound = true)
{
return collect($attributes)
->map(function (string $value, string $attribute) use ($escapeBound) {
- $attributeVariable = '$attribute_'.md5($attribute);
-
return $escapeBound && isset($this->boundAttributes[$attribute]) && $value !== 'true' && ! is_numeric($value)
? "'{$attribute}' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute({$value})"
: "'{$attribute}' => {$value}";
.../Compilers/Concerns/CompilesComponents.php | 2 +-
.../View/Blade/BladeComponentTagCompilerTest.php | 16 ++++++++++++++++
2 files changed, 17 insertions(+), 1 deletion(-)
src/Illuminate/View/Compilers/Concerns/CompilesComponents.php+1 1
@@ -164,7 +164,7 @@ protected function compileProps($expression)
public static function sanitizeComponentAttribute($value)
{
return is_string($value) ||
- (is_object($value) && method_exists($value, '__toString', $value))
+ (is_object($value) && method_exists($value, '__toString'))
? e($value)
: $value;
}
More files changed — see the full commit.

References