Security: fix multiple XSS vulnerabilities + fix search tags with special chars
XSS vulnerabilities fixed in editlink, linklist, tag.cloud and tag.list. Also fixed tag search with special characters: urlencode function needs to be applied on raw data, before espaping, otherwise the rendered URL is wrong.
This commit is contained in:
parent
df25b28dcd
commit
72fbbcd679
11 changed files with 68 additions and 27 deletions
|
@ -95,14 +95,14 @@ function escape($input)
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_bool($input)) {
|
if (is_bool($input) || is_int($input) || is_float($input) || $input instanceof DateTimeInterface) {
|
||||||
return $input;
|
return $input;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_array($input)) {
|
if (is_array($input)) {
|
||||||
$out = array();
|
$out = array();
|
||||||
foreach ($input as $key => $value) {
|
foreach ($input as $key => $value) {
|
||||||
$out[$key] = escape($value);
|
$out[escape($key)] = escape($value);
|
||||||
}
|
}
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,9 @@ public function format($bookmark)
|
||||||
$out['title'] = $this->formatTitle($bookmark);
|
$out['title'] = $this->formatTitle($bookmark);
|
||||||
$out['description'] = $this->formatDescription($bookmark);
|
$out['description'] = $this->formatDescription($bookmark);
|
||||||
$out['thumbnail'] = $this->formatThumbnail($bookmark);
|
$out['thumbnail'] = $this->formatThumbnail($bookmark);
|
||||||
|
$out['urlencoded_taglist'] = $this->formatUrlEncodedTagList($bookmark);
|
||||||
$out['taglist'] = $this->formatTagList($bookmark);
|
$out['taglist'] = $this->formatTagList($bookmark);
|
||||||
|
$out['urlencoded_tags'] = $this->formatUrlEncodedTagString($bookmark);
|
||||||
$out['tags'] = $this->formatTagString($bookmark);
|
$out['tags'] = $this->formatTagString($bookmark);
|
||||||
$out['sticky'] = $bookmark->isSticky();
|
$out['sticky'] = $bookmark->isSticky();
|
||||||
$out['private'] = $bookmark->isPrivate();
|
$out['private'] = $bookmark->isPrivate();
|
||||||
|
@ -181,6 +183,18 @@ protected function formatTagList($bookmark)
|
||||||
return $this->filterTagList($bookmark->getTags());
|
return $this->filterTagList($bookmark->getTags());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format Url Encoded Tags
|
||||||
|
*
|
||||||
|
* @param Bookmark $bookmark instance
|
||||||
|
*
|
||||||
|
* @return array formatted Tags
|
||||||
|
*/
|
||||||
|
protected function formatUrlEncodedTagList($bookmark)
|
||||||
|
{
|
||||||
|
return array_map('urlencode', $this->filterTagList($bookmark->getTags()));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format TagString
|
* Format TagString
|
||||||
*
|
*
|
||||||
|
@ -193,6 +207,18 @@ protected function formatTagString($bookmark)
|
||||||
return implode(' ', $this->formatTagList($bookmark));
|
return implode(' ', $this->formatTagList($bookmark));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format TagString
|
||||||
|
*
|
||||||
|
* @param Bookmark $bookmark instance
|
||||||
|
*
|
||||||
|
* @return string formatted TagString
|
||||||
|
*/
|
||||||
|
protected function formatUrlEncodedTagString($bookmark)
|
||||||
|
{
|
||||||
|
return implode(' ', $this->formatUrlEncodedTagList($bookmark));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format Class
|
* Format Class
|
||||||
* Used to add specific CSS class for a link
|
* Used to add specific CSS class for a link
|
||||||
|
|
|
@ -78,13 +78,13 @@ public function displayCreateForm(Request $request, Response $response): Respons
|
||||||
$title = $this->container->conf->get('general.default_note_title', t('Note: '));
|
$title = $this->container->conf->get('general.default_note_title', t('Note: '));
|
||||||
}
|
}
|
||||||
|
|
||||||
$link = escape([
|
$link = [
|
||||||
'title' => $title,
|
'title' => $title,
|
||||||
'url' => $url ?? '',
|
'url' => $url ?? '',
|
||||||
'description' => $description ?? '',
|
'description' => $description ?? '',
|
||||||
'tags' => $tags ?? '',
|
'tags' => $tags ?? '',
|
||||||
'private' => $private,
|
'private' => $private,
|
||||||
]);
|
];
|
||||||
} else {
|
} else {
|
||||||
$formatter = $this->container->formatterFactory->getFormatter('raw');
|
$formatter = $this->container->formatterFactory->getFormatter('raw');
|
||||||
$link = $formatter->format($bookmark);
|
$link = $formatter->format($bookmark);
|
||||||
|
@ -345,14 +345,14 @@ protected function displayForm(array $link, bool $isNew, Request $request, Respo
|
||||||
$tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1;
|
$tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = [
|
$data = escape([
|
||||||
'link' => $link,
|
'link' => $link,
|
||||||
'link_is_new' => $isNew,
|
'link_is_new' => $isNew,
|
||||||
'http_referer' => escape($this->container->environment['HTTP_REFERER'] ?? ''),
|
'http_referer' => $this->container->environment['HTTP_REFERER'] ?? '',
|
||||||
'source' => $request->getParam('source') ?? '',
|
'source' => $request->getParam('source') ?? '',
|
||||||
'tags' => $tags,
|
'tags' => $tags,
|
||||||
'default_private_links' => $this->container->conf->get('privacy.default_private_links', false),
|
'default_private_links' => $this->container->conf->get('privacy.default_private_links', false),
|
||||||
];
|
]);
|
||||||
|
|
||||||
$this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK);
|
$this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK);
|
||||||
|
|
||||||
|
|
|
@ -41,8 +41,8 @@ public function save(Request $request, Response $response): Response
|
||||||
|
|
||||||
$isDelete = null !== $request->getParam('deletetag') && null === $request->getParam('renametag');
|
$isDelete = null !== $request->getParam('deletetag') && null === $request->getParam('renametag');
|
||||||
|
|
||||||
$fromTag = escape(trim($request->getParam('fromtag') ?? ''));
|
$fromTag = trim($request->getParam('fromtag') ?? '');
|
||||||
$toTag = escape(trim($request->getParam('totag') ?? ''));
|
$toTag = trim($request->getParam('totag') ?? '');
|
||||||
|
|
||||||
if (0 === strlen($fromTag) || false === $isDelete && 0 === strlen($toTag)) {
|
if (0 === strlen($fromTag) || false === $isDelete && 0 === strlen($toTag)) {
|
||||||
$this->saveWarningMessage(t('Invalid tags provided.'));
|
$this->saveWarningMessage(t('Invalid tags provided.'));
|
||||||
|
|
|
@ -34,7 +34,7 @@ public function index(Request $request, Response $response): Response
|
||||||
$formatter = $this->container->formatterFactory->getFormatter();
|
$formatter = $this->container->formatterFactory->getFormatter();
|
||||||
$formatter->addContextData('base_path', $this->container->basePath);
|
$formatter->addContextData('base_path', $this->container->basePath);
|
||||||
|
|
||||||
$searchTags = escape(normalize_spaces($request->getParam('searchtags') ?? ''));
|
$searchTags = normalize_spaces($request->getParam('searchtags') ?? '');
|
||||||
$searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? ''));;
|
$searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? ''));;
|
||||||
|
|
||||||
// Filter bookmarks according search parameters.
|
// Filter bookmarks according search parameters.
|
||||||
|
@ -104,8 +104,9 @@ public function index(Request $request, Response $response): Response
|
||||||
'page_current' => $page,
|
'page_current' => $page,
|
||||||
'page_max' => $pageCount,
|
'page_max' => $pageCount,
|
||||||
'result_count' => count($linksToDisplay),
|
'result_count' => count($linksToDisplay),
|
||||||
'search_term' => $searchTerm,
|
'search_term' => escape($searchTerm),
|
||||||
'search_tags' => $searchTags,
|
'search_tags' => escape($searchTags),
|
||||||
|
'search_tags_url' => array_map('urlencode', explode(' ', $searchTags)),
|
||||||
'visibility' => $visibility,
|
'visibility' => $visibility,
|
||||||
'links' => $linkDisp,
|
'links' => $linkDisp,
|
||||||
]
|
]
|
||||||
|
|
|
@ -66,10 +66,18 @@ protected function processRequest(string $type, Request $request, Response $resp
|
||||||
$tags = $this->formatTagsForCloud($tags);
|
$tags = $this->formatTagsForCloud($tags);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$tagsUrl = [];
|
||||||
|
foreach ($tags as $tag => $value) {
|
||||||
|
$tagsUrl[escape($tag)] = urlencode((string) $tag);
|
||||||
|
}
|
||||||
|
|
||||||
$searchTags = implode(' ', escape($filteringTags));
|
$searchTags = implode(' ', escape($filteringTags));
|
||||||
|
$searchTagsUrl = urlencode(implode(' ', $filteringTags));
|
||||||
$data = [
|
$data = [
|
||||||
'search_tags' => $searchTags,
|
'search_tags' => escape($searchTags),
|
||||||
'tags' => $tags,
|
'search_tags_url' => $searchTagsUrl,
|
||||||
|
'tags' => escape($tags),
|
||||||
|
'tags_url' => $tagsUrl,
|
||||||
];
|
];
|
||||||
$this->executePageHooks('render_tag' . $type, $data, 'tag.' . $type);
|
$this->executePageHooks('render_tag' . $type, $data, 'tag.' . $type);
|
||||||
$this->assignAllView($data);
|
$this->assignAllView($data);
|
||||||
|
|
|
@ -137,7 +137,7 @@ private function initialize()
|
||||||
$this->tpl->assign('language', $this->conf->get('translation.language'));
|
$this->tpl->assign('language', $this->conf->get('translation.language'));
|
||||||
|
|
||||||
if ($this->bookmarkService !== null) {
|
if ($this->bookmarkService !== null) {
|
||||||
$this->tpl->assign('tags', $this->bookmarkService->bookmarksCountPerTag());
|
$this->tpl->assign('tags', escape($this->bookmarkService->bookmarksCountPerTag()));
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->tpl->assign(
|
$this->tpl->assign(
|
||||||
|
|
|
@ -555,6 +555,7 @@ function init(description) {
|
||||||
}
|
}
|
||||||
const refreshedToken = document.getElementById('token').value;
|
const refreshedToken = document.getElementById('token').value;
|
||||||
const fromtag = block.getAttribute('data-tag');
|
const fromtag = block.getAttribute('data-tag');
|
||||||
|
const fromtagUrl = block.getAttribute('data-tag-url');
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.open('POST', `${basePath}/admin/tags`);
|
xhr.open('POST', `${basePath}/admin/tags`);
|
||||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||||
|
@ -564,6 +565,7 @@ function init(description) {
|
||||||
location.reload();
|
location.reload();
|
||||||
} else {
|
} else {
|
||||||
block.setAttribute('data-tag', totag);
|
block.setAttribute('data-tag', totag);
|
||||||
|
block.setAttribute('data-tag-url', encodeURIComponent(totag));
|
||||||
input.setAttribute('name', totag);
|
input.setAttribute('name', totag);
|
||||||
input.setAttribute('value', totag);
|
input.setAttribute('value', totag);
|
||||||
findParent(input, 'div', { class: 'rename-tag-form' }).style.display = 'none';
|
findParent(input, 'div', { class: 'rename-tag-form' }).style.display = 'none';
|
||||||
|
@ -571,6 +573,9 @@ function init(description) {
|
||||||
block
|
block
|
||||||
.querySelector('a.tag-link')
|
.querySelector('a.tag-link')
|
||||||
.setAttribute('href', `${basePath}/?searchtags=${encodeURIComponent(totag)}`);
|
.setAttribute('href', `${basePath}/?searchtags=${encodeURIComponent(totag)}`);
|
||||||
|
block
|
||||||
|
.querySelector('a.count')
|
||||||
|
.setAttribute('href', `${basePath}/add-tag/${encodeURIComponent(totag)}`);
|
||||||
block
|
block
|
||||||
.querySelector('a.rename-tag')
|
.querySelector('a.rename-tag')
|
||||||
.setAttribute('href', `${basePath}/admin/tags?fromtag=${encodeURIComponent(totag)}`);
|
.setAttribute('href', `${basePath}/admin/tags?fromtag=${encodeURIComponent(totag)}`);
|
||||||
|
@ -580,7 +585,7 @@ function init(description) {
|
||||||
awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);
|
awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
xhr.send(`renametag=1&fromtag=${encodeURIComponent(fromtag)}&totag=${encodeURIComponent(totag)}&token=${refreshedToken}`);
|
xhr.send(`renametag=1&fromtag=${fromtagUrl}&totag=${encodeURIComponent(totag)}&token=${refreshedToken}`);
|
||||||
refreshToken(basePath);
|
refreshToken(basePath);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -603,6 +608,7 @@ function init(description) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const block = findParent(event.target, 'div', { class: 'tag-list-item' });
|
const block = findParent(event.target, 'div', { class: 'tag-list-item' });
|
||||||
const tag = block.getAttribute('data-tag');
|
const tag = block.getAttribute('data-tag');
|
||||||
|
const tagUrl = block.getAttribute('data-tag-url');
|
||||||
const refreshedToken = document.getElementById('token').value;
|
const refreshedToken = document.getElementById('token').value;
|
||||||
|
|
||||||
if (confirm(`Are you sure you want to delete the tag "${tag}"?`)) {
|
if (confirm(`Are you sure you want to delete the tag "${tag}"?`)) {
|
||||||
|
@ -612,7 +618,7 @@ function init(description) {
|
||||||
xhr.onload = () => {
|
xhr.onload = () => {
|
||||||
block.remove();
|
block.remove();
|
||||||
};
|
};
|
||||||
xhr.send(encodeURI(`deletetag=1&fromtag=${tag}&token=${refreshedToken}`));
|
xhr.send(`deletetag=1&fromtag=${tagUrl}&token=${refreshedToken}`);
|
||||||
refreshToken(basePath);
|
refreshToken(basePath);
|
||||||
|
|
||||||
existingTags = existingTags.filter((tagItem) => tagItem !== tag);
|
existingTags = existingTags.filter((tagItem) => tagItem !== tag);
|
||||||
|
|
|
@ -94,7 +94,7 @@
|
||||||
{'tagged'|t}
|
{'tagged'|t}
|
||||||
{loop="$exploded_tags"}
|
{loop="$exploded_tags"}
|
||||||
<span class="label label-tag" title="{'Remove tag'|t}">
|
<span class="label label-tag" title="{'Remove tag'|t}">
|
||||||
<a href="{$base_path}/remove-tag/{function="urlencode($value)"}" aria-label="{'Remove tag'|t}">
|
<a href="{$base_path}/remove-tag/{function="$search_tags_url.$key1"}" aria-label="{'Remove tag'|t}">
|
||||||
{$value}<span class="remove"><i class="fa fa-times" aria-hidden="true"></i></span>
|
{$value}<span class="remove"><i class="fa fa-times" aria-hidden="true"></i></span>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
@ -183,7 +183,7 @@ <h2>
|
||||||
{$tag_counter=count($value.taglist)}
|
{$tag_counter=count($value.taglist)}
|
||||||
{loop="value.taglist"}
|
{loop="value.taglist"}
|
||||||
<span class="label label-tag" title="{$strAddTag}">
|
<span class="label label-tag" title="{$strAddTag}">
|
||||||
<a href="{$base_path}/add-tag/{$value|urlencode}">{$value}</a>
|
<a href="{$base_path}/add-tag/{$value1.urlencoded_taglist.$key2}">{$value}</a>
|
||||||
</span>
|
</span>
|
||||||
{if="$tag_counter - 1 != $counter"}·{/if}
|
{if="$tag_counter - 1 != $counter"}·{/if}
|
||||||
{/loop}
|
{/loop}
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<h2 class="window-title">{'Tag cloud'|t} - {$countTags} {'tags'|t}</h2>
|
<h2 class="window-title">{'Tag cloud'|t} - {$countTags} {'tags'|t}</h2>
|
||||||
{if="!empty($search_tags)"}
|
{if="!empty($search_tags)"}
|
||||||
<p class="center">
|
<p class="center">
|
||||||
<a href="{$base_path}/?searchtags={$search_tags|urlencode}" class="pure-button pure-button-shaarli">
|
<a href="{$base_path}/?searchtags={$search_tags_url}" class="pure-button pure-button-shaarli">
|
||||||
{'List all links with those tags'|t}
|
{'List all links with those tags'|t}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
@ -48,8 +48,8 @@ <h2 class="window-title">{'Tag cloud'|t} - {$countTags} {'tags'|t}</h2>
|
||||||
|
|
||||||
<div id="cloudtag" class="cloudtag-container">
|
<div id="cloudtag" class="cloudtag-container">
|
||||||
{loop="tags"}
|
{loop="tags"}
|
||||||
<a href="{$base_path}/?searchtags={$key|urlencode} {$search_tags|urlencode}" style="font-size:{$value.size}em;">{$key}</a
|
<a href="{$base_path}/?searchtags={$tags_url.$key1} {$search_tags_url}" style="font-size:{$value.size}em;">{$key}</a
|
||||||
><a href="{$base_path}/add-tag/{$key|urlencode}" title="{'Filter by tag'|t}" class="count">{$value.count}</a>
|
><a href="{$base_path}/add-tag/{$tags_url.$key1}" title="{'Filter by tag'|t}" class="count">{$value.count}</a>
|
||||||
{loop="$value.tag_plugin"}
|
{loop="$value.tag_plugin"}
|
||||||
{$value}
|
{$value}
|
||||||
{/loop}
|
{/loop}
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<h2 class="window-title">{'Tag list'|t} - {$countTags} {'tags'|t}</h2>
|
<h2 class="window-title">{'Tag list'|t} - {$countTags} {'tags'|t}</h2>
|
||||||
{if="!empty($search_tags)"}
|
{if="!empty($search_tags)"}
|
||||||
<p class="center">
|
<p class="center">
|
||||||
<a href="{$base_path}/?searchtags={$search_tags|urlencode}" class="pure-button pure-button-shaarli">
|
<a href="{$base_path}/?searchtags={$search_tags_url}" class="pure-button pure-button-shaarli">
|
||||||
{'List all links with those tags'|t}
|
{'List all links with those tags'|t}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
@ -47,17 +47,17 @@ <h2 class="window-title">{'Tag list'|t} - {$countTags} {'tags'|t}</h2>
|
||||||
|
|
||||||
<div id="taglist" class="taglist-container">
|
<div id="taglist" class="taglist-container">
|
||||||
{loop="tags"}
|
{loop="tags"}
|
||||||
<div class="tag-list-item pure-g" data-tag="{$key}">
|
<div class="tag-list-item pure-g" data-tag="{$key}" data-tag-url="{$tags_url.$key1}">
|
||||||
<div class="pure-u-1">
|
<div class="pure-u-1">
|
||||||
{if="$is_logged_in===true"}
|
{if="$is_logged_in===true"}
|
||||||
<a href="#" class="delete-tag" aria-label="{'Delete'|t}"><i class="fa fa-trash" aria-hidden="true"></i></a>
|
<a href="#" class="delete-tag" aria-label="{'Delete'|t}"><i class="fa fa-trash" aria-hidden="true"></i></a>
|
||||||
<a href="{$base_path}/admin/tags?fromtag={$key|urlencode}" class="rename-tag" aria-label="{'Rename tag'|t}">
|
<a href="{$base_path}/admin/tags?fromtag={$tags_url.$key1}" class="rename-tag" aria-label="{'Rename tag'|t}">
|
||||||
<i class="fa fa-pencil-square-o {$key}" aria-hidden="true"></i>
|
<i class="fa fa-pencil-square-o {$key}" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<a href="{$base_path}/add-tag/{$key|urlencode}" title="{'Filter by tag'|t}" class="count">{$value}</a>
|
<a href="{$base_path}/add-tag/{$tags_url.$key1}" title="{'Filter by tag'|t}" class="count">{$value}</a>
|
||||||
<a href="{$base_path}/?searchtags={$key|urlencode} {$search_tags|urlencode}" class="tag-link">{$key}</a>
|
<a href="{$base_path}/?searchtags={$tags_url.$key1} {$search_tags_url}" class="tag-link">{$key}</a>
|
||||||
|
|
||||||
{loop="$value.tag_plugin"}
|
{loop="$value.tag_plugin"}
|
||||||
{$value}
|
{$value}
|
||||||
|
|
Loading…
Reference in a new issue