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.
return null;
}
- if (is_bool($input)) {
+ if (is_bool($input) || is_int($input) || is_float($input) || $input instanceof DateTimeInterface) {
return $input;
}
if (is_array($input)) {
$out = array();
foreach ($input as $key => $value) {
- $out[$key] = escape($value);
+ $out[escape($key)] = escape($value);
}
return $out;
}
$out['title'] = $this->formatTitle($bookmark);
$out['description'] = $this->formatDescription($bookmark);
$out['thumbnail'] = $this->formatThumbnail($bookmark);
+ $out['urlencoded_taglist'] = $this->formatUrlEncodedTagList($bookmark);
$out['taglist'] = $this->formatTagList($bookmark);
+ $out['urlencoded_tags'] = $this->formatUrlEncodedTagString($bookmark);
$out['tags'] = $this->formatTagString($bookmark);
$out['sticky'] = $bookmark->isSticky();
$out['private'] = $bookmark->isPrivate();
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
*
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
* Used to add specific CSS class for a link
$title = $this->container->conf->get('general.default_note_title', t('Note: '));
}
- $link = escape([
+ $link = [
'title' => $title,
'url' => $url ?? '',
'description' => $description ?? '',
'tags' => $tags ?? '',
'private' => $private,
- ]);
+ ];
} else {
$formatter = $this->container->formatterFactory->getFormatter('raw');
$link = $formatter->format($bookmark);
$tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1;
}
- $data = [
+ $data = escape([
'link' => $link,
'link_is_new' => $isNew,
- 'http_referer' => escape($this->container->environment['HTTP_REFERER'] ?? ''),
+ 'http_referer' => $this->container->environment['HTTP_REFERER'] ?? '',
'source' => $request->getParam('source') ?? '',
'tags' => $tags,
'default_private_links' => $this->container->conf->get('privacy.default_private_links', false),
- ];
+ ]);
$this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK);
$isDelete = null !== $request->getParam('deletetag') && null === $request->getParam('renametag');
- $fromTag = escape(trim($request->getParam('fromtag') ?? ''));
- $toTag = escape(trim($request->getParam('totag') ?? ''));
+ $fromTag = trim($request->getParam('fromtag') ?? '');
+ $toTag = trim($request->getParam('totag') ?? '');
if (0 === strlen($fromTag) || false === $isDelete && 0 === strlen($toTag)) {
$this->saveWarningMessage(t('Invalid tags provided.'));
$formatter = $this->container->formatterFactory->getFormatter();
$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') ?? ''));;
// Filter bookmarks according search parameters.
'page_current' => $page,
'page_max' => $pageCount,
'result_count' => count($linksToDisplay),
- 'search_term' => $searchTerm,
- 'search_tags' => $searchTags,
+ 'search_term' => escape($searchTerm),
+ 'search_tags' => escape($searchTags),
+ 'search_tags_url' => array_map('urlencode', explode(' ', $searchTags)),
'visibility' => $visibility,
'links' => $linkDisp,
]
$tags = $this->formatTagsForCloud($tags);
}
+ $tagsUrl = [];
+ foreach ($tags as $tag => $value) {
+ $tagsUrl[escape($tag)] = urlencode((string) $tag);
+ }
+
$searchTags = implode(' ', escape($filteringTags));
+ $searchTagsUrl = urlencode(implode(' ', $filteringTags));
$data = [
- 'search_tags' => $searchTags,
- 'tags' => $tags,
+ 'search_tags' => escape($searchTags),
+ 'search_tags_url' => $searchTagsUrl,
+ 'tags' => escape($tags),
+ 'tags_url' => $tagsUrl,
];
$this->executePageHooks('render_tag' . $type, $data, 'tag.' . $type);
$this->assignAllView($data);
$this->tpl->assign('language', $this->conf->get('translation.language'));
if ($this->bookmarkService !== null) {
- $this->tpl->assign('tags', $this->bookmarkService->bookmarksCountPerTag());
+ $this->tpl->assign('tags', escape($this->bookmarkService->bookmarksCountPerTag()));
}
$this->tpl->assign(
}
const refreshedToken = document.getElementById('token').value;
const fromtag = block.getAttribute('data-tag');
+ const fromtagUrl = block.getAttribute('data-tag-url');
const xhr = new XMLHttpRequest();
xhr.open('POST', `${basePath}/admin/tags`);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
location.reload();
} else {
block.setAttribute('data-tag', totag);
+ block.setAttribute('data-tag-url', encodeURIComponent(totag));
input.setAttribute('name', totag);
input.setAttribute('value', totag);
findParent(input, 'div', { class: 'rename-tag-form' }).style.display = 'none';
block
.querySelector('a.tag-link')
.setAttribute('href', `${basePath}/?searchtags=${encodeURIComponent(totag)}`);
+ block
+ .querySelector('a.count')
+ .setAttribute('href', `${basePath}/add-tag/${encodeURIComponent(totag)}`);
block
.querySelector('a.rename-tag')
.setAttribute('href', `${basePath}/admin/tags?fromtag=${encodeURIComponent(totag)}`);
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);
});
});
event.preventDefault();
const block = findParent(event.target, 'div', { class: 'tag-list-item' });
const tag = block.getAttribute('data-tag');
+ const tagUrl = block.getAttribute('data-tag-url');
const refreshedToken = document.getElementById('token').value;
if (confirm(`Are you sure you want to delete the tag "${tag}"?`)) {
xhr.onload = () => {
block.remove();
};
- xhr.send(encodeURI(`deletetag=1&fromtag=${tag}&token=${refreshedToken}`));
+ xhr.send(`deletetag=1&fromtag=${tagUrl}&token=${refreshedToken}`);
refreshToken(basePath);
existingTags = existingTags.filter((tagItem) => tagItem !== tag);
{'tagged'|t}
{loop="$exploded_tags"}
<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>
</a>
</span>
{$tag_counter=count($value.taglist)}
{loop="value.taglist"}
<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>
{if="$tag_counter - 1 != $counter"}·{/if}
{/loop}
<h2 class="window-title">{'Tag cloud'|t} - {$countTags} {'tags'|t}</h2>
{if="!empty($search_tags)"}
<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}
</a>
</p>
<div id="cloudtag" class="cloudtag-container">
{loop="tags"}
- <a href="{$base_path}/?searchtags={$key|urlencode} {$search_tags|urlencode}" 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}/?searchtags={$tags_url.$key1} {$search_tags_url}" style="font-size:{$value.size}em;">{$key}</a
+ ><a href="{$base_path}/add-tag/{$tags_url.$key1}" title="{'Filter by tag'|t}" class="count">{$value.count}</a>
{loop="$value.tag_plugin"}
{$value}
{/loop}
<h2 class="window-title">{'Tag list'|t} - {$countTags} {'tags'|t}</h2>
{if="!empty($search_tags)"}
<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}
</a>
</p>
<div id="taglist" class="taglist-container">
{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">
{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="{$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>
</a>
{/if}
- <a href="{$base_path}/add-tag/{$key|urlencode}" 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}/add-tag/{$tags_url.$key1}" title="{'Filter by tag'|t}" class="count">{$value}</a>
+ <a href="{$base_path}/?searchtags={$tags_url.$key1} {$search_tags_url}" class="tag-link">{$key}</a>
{loop="$value.tag_plugin"}
{$value}