Skip to content

Commit

Permalink
[SECURITY] Avoid XSS by correctly encoding typolink results
Browse files Browse the repository at this point in the history
In order to avoid XSS through typolink, anchor text is encoded
correctly to be used in a HTML context. Fallback link texts of
links to pages are encoded per default in case lib.parseFunc
has not been configured.

Resolves: #88635
Releases: master, 9.5, 8.7
Security-Commit: 57c5eeb93e6df4b1958bcafcd85ada6c7e355d41
Security-Bulletin: TYPO3-CORE-SA-2019-022
Change-Id: I9a415d6b2ed494dac7f4747e25460d95e1f27284
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/62704
Tested-by: Oliver Hader <oliver.hader@typo3.org>
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
  • Loading branch information
ohader committed Dec 17, 2019
1 parent 9692bf8 commit 64db88b
Show file tree
Hide file tree
Showing 8 changed files with 509 additions and 32 deletions.
1 change: 1 addition & 0 deletions typo3/sysext/core/Classes/LinkHandling/UrlLinkHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ protected function addHttpSchemeAsFallback(string $url): string
$scheme = parse_url($url, PHP_URL_SCHEME);
if (empty($scheme)) {
$url = 'http://' . $url;
// 'java{TAB}script:' is parsed as empty URL scheme, thus not ending up here
} elseif (in_array(strtolower($scheme), ['javascript', 'data'], true)) {
// deny using insecure scheme's like `javascript:` or `data:` as URL scheme
$url = '';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5349,7 +5349,7 @@ protected function resolveMixedLinkParameter($linkText, $mixedLinkParameter, &$c
// Resource was not found
return $linkText;
}
} elseif (in_array(strtolower(trim($linkHandlerKeyword)), ['javascript', 'data'], true)) {
} elseif (in_array(strtolower(preg_replace('#\s|[[:cntrl:]]#', '', $linkHandlerKeyword)), ['javascript', 'data'], true)) {
// Disallow insecure scheme's like javascript: or data:
return $linkText;
} else {
Expand Down
37 changes: 35 additions & 2 deletions typo3/sysext/frontend/Classes/Typolink/AbstractTypolinkBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,20 @@ protected function forceAbsoluteUrl(string $url, array $configuration): string
return $url;
}

/**
* Determines whether lib.parseFunc is defined.
*
* @return bool
*/
protected function isLibParseFuncDefined(): bool
{
$configuration = $this->contentObjectRenderer->mergeTSRef(
['parseFunc' => '< lib.parseFunc'],
'parseFunc'
);
return !empty($configuration['parseFunc.']) && is_array($configuration['parseFunc.']);
}

/**
* Helper method to a fallback method parsing HTML out of it
*
Expand All @@ -118,10 +132,29 @@ protected function forceAbsoluteUrl(string $url, array $configuration): string
*/
protected function parseFallbackLinkTextIfLinkTextIsEmpty(string $originalLinkText, string $fallbackLinkText): string
{
if ($originalLinkText === '') {
if ($originalLinkText !== '') {
return $originalLinkText;
}
if ($this->isLibParseFuncDefined()) {
return $this->contentObjectRenderer->parseFunc($fallbackLinkText, ['makelinks' => 0], '< lib.parseFunc');
}
return $originalLinkText;
// encode in case `lib.parseFunc` is not configured
return $this->encodeFallbackLinkTextIfLinkTextIsEmpty($originalLinkText, $fallbackLinkText);
}

/**
* Helper method to a fallback method properly encoding HTML.
*
* @param string $originalLinkText the original string, if empty, the fallback link text
* @param string $fallbackLinkText the string to be used.
* @return string the final text
*/
protected function encodeFallbackLinkTextIfLinkTextIsEmpty(string $originalLinkText, string $fallbackLinkText): string
{
if ($originalLinkText !== '') {
return $originalLinkText;
}
return htmlspecialchars($fallbackLinkText, ENT_QUOTES);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public function build(array &$linkDetails, string $linkText, string $target, arr
{
return [
$this->processUrl(UrlProcessorInterface::CONTEXT_EXTERNAL, $linkDetails['url'], $conf),
$this->parseFallbackLinkTextIfLinkTextIsEmpty($linkText, $linkDetails['url']),
$this->encodeFallbackLinkTextIfLinkTextIsEmpty($linkText, $linkDetails['url']),
$target ?: $this->resolveTargetAttribute($conf, 'extTarget', true, $this->getTypoScriptFrontendController()->extTarget)
];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public function build(array &$linkDetails, string $linkText, string $target, arr
$linkLocation = '';
}
// Setting title if blank value to link
$linkText = $this->parseFallbackLinkTextIfLinkTextIsEmpty($linkText, rawurldecode($linkLocation));
$linkText = $this->encodeFallbackLinkTextIfLinkTextIsEmpty($linkText, rawurldecode($linkLocation));
if (strpos($linkLocation, '/') !== 0
&& parse_url($linkLocation, PHP_URL_SCHEME) === null
) {
Expand Down
4 changes: 2 additions & 2 deletions typo3/sysext/frontend/Classes/Typolink/LegacyLinkBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ public function build(array &$linkDetails, string $linkText, string $target, arr
$linkDetails['type'] = LinkService::TYPE_FILE;
$linkLocation = $linkDetails['file'];
// Setting title if blank value to link
$linkText = $this->parseFallbackLinkTextIfLinkTextIsEmpty($linkText, rawurldecode($linkLocation));
$linkText = $this->encodeFallbackLinkTextIfLinkTextIsEmpty($linkText, rawurldecode($linkLocation));
$linkLocation = (strpos($linkLocation, '/') !== 0 ? $tsfe->absRefPrefix : '') . $linkLocation;
$url = $this->processUrl(UrlProcessorInterface::CONTEXT_FILE, $linkLocation, $conf);
$url = $this->forceAbsoluteUrl($url, $conf);
$target = $target ?: $this->resolveTargetAttribute($conf, 'fileTarget', false, $tsfe->fileTarget);
} elseif ($linkDetails['url']) {
$linkDetails['type'] = LinkService::TYPE_URL;
$target = $target ?: $this->resolveTargetAttribute($conf, 'extTarget', true, $tsfe->extTarget);
$linkText = $this->parseFallbackLinkTextIfLinkTextIsEmpty($linkText, $linkDetails['url']);
$linkText = $this->encodeFallbackLinkTextIfLinkTextIsEmpty($linkText, $linkDetails['url']);
$url = $this->processUrl(UrlProcessorInterface::CONTEXT_EXTERNAL, $linkDetails['url'], $conf);
} else {
throw new UnableToLinkException('Unknown link detected, so ' . $linkText . ' was not linked.', 1490990031, null, $linkText);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
__variables:
- &pageStandard 0
- &pageShortcut 4

entitySettings:
'*':
nodeColumnName: 'pid'
columnNames: {id: 'uid', language: 'sys_language_uid'}
defaultValues: {pid: 0}
page:
isNode: true
tableName: 'pages'
parentColumnName: 'pid'
languageColumnNames: ['l10n_parent', 'l10n_source']
columnNames: {type: 'doktype', root: 'is_siteroot'}
defaultValues: {hidden: 0, doktype: *pageStandard}
valueInstructions:
shortcut:
first: {shortcut: 0, shortcut_mode: 1}
content:
tableName: 'tt_content'
languageColumnNames: ['l18n_parent', 'l10n_source']
columnNames: {title: 'header', type: 'CType'}
defaultValues: {hidden: 0, CType: 'text'}

entities:
page:
- self: {id: 1000, title: 'ACME Inc', type: *pageShortcut, shortcut: 'first', root: true, slug: '/'}
children:
- self: {id: 1100, title: 'EN: Welcome', slug: '/welcome', subtitle: 'hello-and-welcome'}
entities:
content:
- self: {title: 'EN: Content Element #1'}
- self: {title: 'EN: Content Element #2'}
- self: {id: 1200, title: 'EN: Features', slug: '/features'}
- self: {id: 9911, title: '<good>', slug: '/test/good'}
- self: {id: 9912, title: '<good a="a/" b="thing(1)">', slug: '/test/good-a-b-spaced'}
- self: {id: 9913, title: '<good%20a="a/"%20b="thing(1)">', slug: '/test/good-a-b-enc-a'}
- self: {id: 9914, title: '<good/a="a/"/b="thing(1)">', slug: '/test/good-a-b-enc-b'}
- self: {id: 9921, title: '<bad>', slug: '/test/bad'}
Loading

0 comments on commit 64db88b

Please sign in to comment.