Skip to content

Update HTML filters #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 7 additions & 21 deletions .github/workflows/code_analysis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,18 @@ jobs:
run: vendor/bin/tester tests -s -C

versions:
- name: newest
arg: ''
- name: highest
experimental: false

- name: lowest
arg: '--prefer-lowest'
experimental: true

name: ${{ matrix.actions.name }} at PHP ${{ matrix.php }} (${{ matrix.versions.name }})
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v2

uses: actions/checkout@v4

# see https://github.com/shivammathur/setup-php
- name: Setup PHP
Expand All @@ -51,22 +48,11 @@ jobs:
extensions: json
coverage: none


# see https://github.com/actions/cache/blob/main/examples.md#php---composer
- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
- uses: actions/cache@v2
- name: Install Composer deps
uses: ramsey/composer-install@v3
with:
path: |
${{ steps.composer-cache.outputs.dir }}
**/composer.lock
key: ${{ runner.os }}-${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }}


- name: Install Composer
run: composer update --no-progress ${{ matrix.versions.arg }}
dependency-versions: "${{ matrix.versions.name }}"

- run: ${{ matrix.actions.run }}
- name: Run ${{ matrix.actions.name }}
run: ${{ matrix.actions.run }}
continue-on-error: ${{ matrix.versions.experimental }}
31 changes: 23 additions & 8 deletions src/Escape.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
class Escape
{
/**
* Escapes strings for use everywhere inside HTML (except for comments) and concatenate it to string.
* Escapes strings for use inside HTML text and concatenate it to string.
* @param string|HtmlStringable|IHtmlString|mixed ...$data
* @return string
*
Expand All @@ -33,7 +33,9 @@ public static function html(...$data): string
if ($item instanceof HtmlStringable || $item instanceof IHtmlString) {
$output .= $item;
} else {
$output .= htmlspecialchars((string)$item, ENT_QUOTES | ENT_HTML5 | ENT_SUBSTITUTE);
$str = htmlspecialchars((string)$item, ENT_QUOTES | ENT_HTML5 | ENT_SUBSTITUTE, 'UTF-8');
$str = strtr($str, ['{{' => '{<!-- -->{', '{' => '&#123;']);
$output .= $str;
}
}

Expand All @@ -50,10 +52,23 @@ public static function html(...$data): string
public static function htmlAttr($data): string
{
$data = (string)$data;
if (strpos($data, '`') !== false && strpbrk($data, ' <>"\'') === false) {
$data .= ' '; // protection against innerHTML mXSS vulnerability nette/nette#1496
}
return self::html($data);
$data = htmlspecialchars($data, ENT_QUOTES | ENT_HTML5 | ENT_SUBSTITUTE, 'UTF-8');
$data = str_replace('{', '&#123;', $data);
return $data;
}

/**
* Escapes JSON data for use inside HTML attribute value (especially for data attributes).
* @param array|mixed $data
* @return string
*/
public static function htmlJsonAttr($data): string
{
$json = json_encode(
$data,
JSON_HEX_AMP | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE
);
return self::htmlAttr($json);
}

/**
Expand Down Expand Up @@ -158,13 +173,13 @@ public static function url($data): string
*
* @link https://api.nette.org/2.4/source-Latte.Runtime.Filters.php.html#_safeUrl
*/
public static function safeUrl($data, bool $warning = false):string
public static function safeUrl($data, bool $warning = false): string
{
if (preg_match('~^(?:(?:https?|ftp)://[^@]+(?:/.*)?|(?:mailto|tel|sms):.+|[/?#].*|[^:]+)$~Di', (string)$data)) {
return (string)$data;
}

if($warning) {
if ($warning) {
trigger_error('URL was removed because is invalid or unsafe: ' . $data, E_USER_WARNING);
}

Expand Down
35 changes: 31 additions & 4 deletions tests/EscapeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public function getHtmlArgs(): array
['Hello &lt;World&gt;Hello <World>', ['Hello <World>', Html::fromHtml('Hello <World>')]],
['Hello <World>Hello &lt;World&gt;', [Html::fromHtml('Hello <World>'), 'Hello <World>']],
['Hello <World>Hello <World>', [Html::fromHtml('Hello <World>'), Html::fromHtml('Hello <World>')]],
['Hello {<!-- -->{my}} lord', ['Hello {{my}} lord']],
];
}

Expand All @@ -63,18 +64,19 @@ public function getHtmlAttrArgs(): array
['string', 'string'],
['&lt; &amp; &apos; &quot; &gt;', '< & \' " >'],
['&amp;quot;', '&quot;'],
['`hello ', '`hello'],
['`hello', '`hello'],
['`hello&quot;', '`hello"'],
['`hello&apos;', "`hello'"],
["foo \u{FFFD} bar", "foo \u{D800} bar"], // invalid codepoint high surrogates
["foo \u{FFFD}&quot; bar", "foo \xE3\x80\x22 bar"], // stripped UTF
['Hello World', 'Hello World'],
['Hello &lt;World&gt;', 'Hello <World>'],
['&quot; &apos; &lt; &gt; &amp; �', "\" ' < > & \x8F"],
['`hello` ', '`hello`'],
['``onmouseover=alert(1) ', '``onmouseover=alert(1)'],
['`hello`', '`hello`'],
['``onmouseover=alert(1)', '``onmouseover=alert(1)'],
['` &lt;br&gt; `', '` <br> `'],
['Foo&lt;br&gt;bar', Html::fromHtml('Foo<br>bar')]
['Foo&lt;br&gt;bar', Html::fromHtml('Foo<br>bar')],
['Hello &#123;&#123;my}} lord', 'Hello {{my}} lord'],
];
}

Expand All @@ -86,6 +88,31 @@ public function testHtmlAttr(string $expected, $data): void
Assert::same($expected, Escape::htmlAttr($data));
}

public function getHtmlJsonAttrArgs(): array
{
return [
['null', null],
['&quot;&quot;', ''],
['1', 1],
['&quot;string&quot;', 'string'],
['&quot;&lt;/tag&quot;', '</tag'],
['&quot;\u2028 \u2029 ]]&gt; &lt;!&quot;', "\u{2028} \u{2029} ]]> <!"],
['[0,1]', [0, 1]],
['[&quot;0&quot;,&quot;1&quot;]', ['0', '1']],
['&#123;&quot;a&quot;:&quot;0&quot;,&quot;b&quot;:&quot;1&quot;}', ['a' => '0', 'b' => '1']],
['&#123;&quot;a&quot;:&quot;Hello &#123;&#123;my}} world&quot;,&quot;b&quot;:&quot;1&quot;}', ['a' => 'Hello {{my}} world', 'b' => '1']],
['&quot;&lt;/script&gt;&quot;', '</script>'],
];
}

/**
* @dataProvider getHtmlJsonAttrArgs
*/
public function testHtmlJsonAttr($expected, $data): void
{
Assert::same($expected, Escape::htmlJsonAttr($data));
}

public function getHtmlHrefArgs(): array
{
return [
Expand Down
Loading