Merge pull request #5567 from blockscout/vb-safe-token-metadata

Sanitize token name and symbol before insert into DB, display in the application
pull/5585/head
Victor Baranov 3 years ago committed by GitHub
commit 533549bd87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 31
      apps/block_scout_web/assets/__tests__/lib/autocomplete.js
  3. 12
      apps/block_scout_web/assets/__tests__/lib/utils.js
  4. 40
      apps/block_scout_web/assets/js/lib/autocomplete.js
  5. 15
      apps/block_scout_web/assets/js/lib/try_api.js
  6. 12
      apps/block_scout_web/assets/js/lib/utils.js
  7. 7
      apps/block_scout_web/lib/block_scout_web/views/search_view.ex
  8. 2
      apps/block_scout_web/mix.exs
  9. 44
      apps/block_scout_web/test/block_scout_web/views/search_view_test.exs
  10. 20
      apps/explorer/lib/explorer/chain/token.ex
  11. 1
      apps/explorer/mix.exs
  12. 8
      mix.lock

@ -4,6 +4,7 @@
- [#5540](https://github.com/blockscout/blockscout/pull/5540) - Tx page: scroll to selected tab's data
### Fixes
- [#5567](https://github.com/blockscout/blockscout/pull/5567) - Sanitize token name and symbol before insert into DB, display in the application
- [#5564](https://github.com/blockscout/blockscout/pull/5564) - Add fallback clauses to `string_to_..._hash` functions
- [#5538](https://github.com/blockscout/blockscout/pull/5538) - Fix internal transaction's tile bug

@ -0,0 +1,31 @@
/**
* @jest-environment jsdom
*/
import { searchEngine } from '../../js/lib/autocomplete'
test('searchEngine', () => {
expect(searchEngine('qwe', {
'name': 'Test',
'symbol': 'TST',
'address_hash': '0x000',
'tx_hash': '0x000',
'block_hash': '0x000'
})).toEqual(undefined)
expect(searchEngine('tes', {
'name': 'Test',
'symbol': 'TST',
'address_hash': '0x000',
'tx_hash': '0x000',
'block_hash': '0x000'
})).toEqual('<div><div>0x000</div><div><b><mark class=\'autoComplete_highlight\'>Tes</mark>t</b> (TST)</div></div>')
expect(searchEngine('qwe', {
'name': 'qwe1\'"><iframe/onload=console.log(123)>${7*7}{{7*7}}{{\'7\'*\'7\'}}',
'symbol': 'qwe1\'"><iframe/onload=console.log(123)>${7*7}{{7*7}}{{\'7\'*\'7\'}}',
'address_hash': '0x000',
'tx_hash': '0x000',
'block_hash': '0x000'
})).toEqual('<div><div>0x000</div><div><b><mark class=\'autoComplete_highlight\'>qwe</mark>1&#039;&quot;&gt;&lt;iframe/onload=console.log(123)&gt;${7*7}{{7*7}}{{&#039;7&#039;*&#039;7&#039;}}</b> (<mark class=\'autoComplete_highlight\'>qwe</mark>1&#039;&quot;&gt;&lt;iframe/onload=console.log(123)&gt;${7*7}{{7*7}}{{&#039;7&#039;*&#039;7&#039;}})</div></div>')
})

@ -0,0 +1,12 @@
/**
* @jest-environment jsdom
*/
import { escapeHtml } from '../../js/lib/utils'
test('escapeHtml', () => {
expect(escapeHtml('<script>')).toEqual('&lt;script&gt;')
expect(escapeHtml('1&')).toEqual('1&amp;')
expect(escapeHtml('1"')).toEqual('1&quot;')
expect(escapeHtml('1\'')).toEqual('1&#039;')
})

@ -3,6 +3,7 @@ import AutoComplete from '@tarekraafat/autocomplete.js/dist/autoComplete'
import { getTextAdData, fetchTextAdData } from './ad'
import { DateTime } from 'luxon'
import { appendTokenIcon } from './token_icon'
import { escapeHtml } from './utils'
import xss from 'xss'
const placeHolder = 'Search by address, token symbol, name, transaction hash, or block number'
@ -45,13 +46,14 @@ const resultsListElement = (list, data) => {
fetchTextAdData()
}
const searchEngine = (query, record) => {
export const searchEngine = (query, record) => {
const queryLowerCase = query.toLowerCase()
if (record && (
(record.name && record.name.toLowerCase().includes(query.toLowerCase())) ||
(record.symbol && record.symbol.toLowerCase().includes(query.toLowerCase())) ||
(record.address_hash && record.address_hash.toLowerCase().includes(query.toLowerCase())) ||
(record.tx_hash && record.tx_hash.toLowerCase().includes(query.toLowerCase())) ||
(record.block_hash && record.block_hash.toLowerCase().includes(query.toLowerCase()))
(record.name && record.name.toLowerCase().includes(queryLowerCase)) ||
(record.symbol && record.symbol.toLowerCase().includes(queryLowerCase)) ||
(record.address_hash && record.address_hash.toLowerCase().includes(queryLowerCase)) ||
(record.tx_hash && record.tx_hash.toLowerCase().includes(queryLowerCase)) ||
(record.block_hash && record.block_hash.toLowerCase().includes(queryLowerCase))
)
) {
let searchResult = '<div>'
@ -62,10 +64,10 @@ const searchEngine = (query, record) => {
} else {
searchResult += '<div>'
if (record.name) {
searchResult += `<b>${record.name}</b>`
searchResult += `<b>${escapeHtml(record.name)}</b>`
}
if (record.symbol) {
searchResult += ` (${record.symbol})`
searchResult += ` (${escapeHtml(record.symbol)})`
}
if (record.holder_count) {
searchResult += ` <i>${record.holder_count} holder(s)</i>`
@ -131,9 +133,9 @@ const config = (id) => {
}
}
}
const autoCompleteJS = new AutoComplete(config('main-search-autocomplete'))
const autoCompleteJS = document.querySelector('#main-search-autocomplete') && new AutoComplete(config('main-search-autocomplete'))
// eslint-disable-next-line
const autoCompleteJSMobile = new AutoComplete(config('main-search-autocomplete-mobile'))
const autoCompleteJSMobile = document.querySelector('#main-search-autocomplete-mobile') && new AutoComplete(config('main-search-autocomplete-mobile'))
const selection = (event) => {
const selectionValue = event.detail.selection.value
@ -149,13 +151,6 @@ const selection = (event) => {
}
}
document.querySelector('#main-search-autocomplete').addEventListener('selection', function (event) {
selection(event)
})
document.querySelector('#main-search-autocomplete-mobile').addEventListener('selection', function (event) {
selection(event)
})
const openOnFocus = (event, type) => {
const query = event.target.value
if (query) {
@ -178,10 +173,17 @@ const openOnFocus = (event, type) => {
}
}
document.querySelector('#main-search-autocomplete').addEventListener('focus', function (event) {
document.querySelector('#main-search-autocomplete') && document.querySelector('#main-search-autocomplete').addEventListener('selection', function (event) {
selection(event)
})
document.querySelector('#main-search-autocomplete-mobile') && document.querySelector('#main-search-autocomplete-mobile').addEventListener('selection', function (event) {
selection(event)
})
document.querySelector('#main-search-autocomplete') && document.querySelector('#main-search-autocomplete').addEventListener('focus', function (event) {
openOnFocus(event, 'desktop')
})
document.querySelector('#main-search-autocomplete-mobile').addEventListener('focus', function (event) {
document.querySelector('#main-search-autocomplete-mobile') && document.querySelector('#main-search-autocomplete-mobile').addEventListener('focus', function (event) {
openOnFocus(event, 'mobile')
})

@ -1,5 +1,6 @@
import $ from 'jquery'
import '../app'
import { escapeHtml } from './utils'
// This file adds event handlers responsible for the 'Try it out' UI in the
// Etherscan-compatible API documentation page.
@ -49,25 +50,13 @@ function handleSuccess (query, xhr, clickedButton) {
curl.innerHTML = composeCurlCommand(url)
requestUrl.innerHTML = url
code.innerHTML = xhr.status
body.innerHTML = JSON.stringify(xhr.responseJSON, undefined, 2)
body.innerHTML = escapeHtml(JSON.stringify(xhr.responseJSON, undefined, 2))
$(`[data-selector="${module}-${action}-try-api-ui-result"]`).show()
$(`[data-selector="${module}-${action}-btn-try-api-clear"]`).show()
clickedButton.html(clickedButton.data('original-text'))
clickedButton.prop('disabled', false)
}
function escapeHtml (text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
}
return text.replace(/[&<>"']/g, function (m) { return map[m] })
}
// Show 'Try it out' UI for a module/action.
$('button[data-selector*="btn-try-api"]').click(event => {
const clickedButton = $(event.target)

@ -24,3 +24,15 @@ export function showLoader (isTimeout, loader) {
return null
}
}
export function escapeHtml (text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
}
return text.replace(/[&<>"']/g, function (m) { return map[m] })
}

@ -7,8 +7,13 @@ defmodule BlockScoutWeb.SearchView do
def highlight_search_result(result, query) do
re = ~r/#{query}/i
safe_result =
result
|> html_escape()
|> safe_to_string()
re
|> Regex.replace(result, "<mark class=\'autoComplete_highlight\'>\\g{0}</mark>", global: true)
|> Regex.replace(safe_result, "<mark class=\'autoComplete_highlight\'>\\g{0}</mark>", global: true)
|> raw()
end
end

@ -97,7 +97,7 @@ defmodule BlockScoutWeb.Mixfile do
{:number, "~> 1.0.1"},
{:phoenix, "== 1.5.13"},
{:phoenix_ecto, "~> 4.1"},
{:phoenix_html, "~> 2.10"},
{:phoenix_html, "== 3.0.4"},
{:phoenix_live_reload, "~> 1.2", only: [:dev]},
{:phoenix_pubsub, "~> 2.0"},
# use `:cowboy` for WebServer with `:plug`

@ -0,0 +1,44 @@
defmodule BlockScoutWeb.SearchViewTest do
use ExUnit.Case
alias BlockScoutWeb.SearchView
test "highlight_search_result/2 returns search result if query doesn't match" do
query = "test"
search_result = "qwerty"
res = SearchView.highlight_search_result(search_result, query)
IO.inspect(res)
assert res == {:safe, search_result}
end
test "highlight_search_result/2 returns safe HTML of unsafe search result if query doesn't match" do
query = "test"
search_result = "qwe1'\"><iframe/onload=console.log(123)>${7*7}{{7*7}}{{'7'*'7'}}"
res = SearchView.highlight_search_result(search_result, query)
IO.inspect(res)
assert res ==
{:safe,
"qwe1&#39;&quot;&gt;&lt;iframe/onload=console.log(123)&gt;${7*7}{{7*7}}{{&#39;7&#39;*&#39;7&#39;}}"}
end
test "highlight_search_result/2 returns highlighted search result if query matches" do
query = "qwe"
search_result = "qwerty"
res = SearchView.highlight_search_result(search_result, query)
IO.inspect(res)
assert res == {:safe, "<mark class='autoComplete_highlight'>qwe</mark>rty"}
end
test "highlight_search_result/2 returns highlighted safe HTML of unsafe search result if query match" do
query = "qwe"
search_result = "qwe1'\"><iframe/onload=console.log(123)>${7*7}{{7*7}}{{'7'*'7'}}"
res = SearchView.highlight_search_result(search_result, query)
IO.inspect(res)
assert res ==
{:safe,
"<mark class='autoComplete_highlight'>qwe</mark>1&#39;&quot;&gt;&lt;iframe/onload=console.log(123)&gt;${7*7}{{7*7}}{{&#39;7&#39;*&#39;7&#39;}}"}
end
end

@ -24,6 +24,7 @@ defmodule Explorer.Chain.Token do
alias Ecto.Changeset
alias Explorer.Chain.{Address, Hash, Token}
alias Phoenix.HTML
@typedoc """
* `name` - Name of the token
@ -102,6 +103,8 @@ defmodule Explorer.Chain.Token do
|> validate_required(@required_attrs)
|> foreign_key_constraint(:contract_address)
|> trim_name()
|> sanitize_input(:name)
|> sanitize_input(:symbol)
|> unique_constraint(:contract_address_hash)
end
@ -114,6 +117,23 @@ defmodule Explorer.Chain.Token do
end
end
defp sanitize_input(%Changeset{valid?: false} = changeset, _), do: changeset
defp sanitize_input(%Changeset{valid?: true} = changeset, key) do
case get_change(changeset, key) do
nil ->
changeset
property ->
safe_property =
property
|> HTML.html_escape()
|> HTML.safe_to_string()
put_change(changeset, key, String.trim(safe_property))
end
end
@doc """
Builds an `Ecto.Query` to fetch the cataloged tokens.

@ -86,6 +86,7 @@ defmodule Explorer.Mixfile do
{:math, "~> 0.3.0"},
{:mock, "~> 0.3.0", only: [:test], runtime: false},
{:mox, "~> 0.4", only: [:test]},
{:phoenix_html, "== 3.0.4"},
{:poison, "~> 4.0.1"},
{:nimble_csv, "~> 1.1"},
{:postgrex, ">= 0.0.0"},

@ -52,7 +52,7 @@
"exvcr": {:hex, :exvcr, "0.13.2", "e17fd3ee3a341f41a3aa65a3ce73a339759a9d0658f83782492c6e9b6cf9daa4", [:mix], [{:exactor, "~> 2.2", [hex: :exactor, repo: "hexpm", optional: false]}, {:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:finch, "~> 0.8.0", [hex: :finch, repo: "hexpm", optional: true]}, {:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.1", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "17f41a533d14f582fe6b5f83214f058cf5ba77c6a7bc15bc53a9ea1827d92d96"},
"file_info": {:hex, :file_info, "0.0.4", "2e0e77f211e833f38ead22cb29ce53761d457d80b3ffe0ffe0eb93880b0963b2", [:mix], [{:mimetype_parser, "~> 0.1.2", [hex: :mimetype_parser, repo: "hexpm", optional: false]}], "hexpm", "50e7ad01c2c8b9339010675fe4dc4a113b8d6ca7eddce24d1d74fd0e762781a5"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"floki": {:hex, :floki, "0.31.0", "f05ee8a8e6a3ced4e62beeb2c79a63bc8e12ab98fbaaf6e6a3d9b76b1278e23f", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "b05afa372f5c345a5bf240ac25ea1f0f3d5fcfd7490ac0beeb4a203f9444891e"},
"floki": {:hex, :floki, "0.32.1", "dfe3b8db3b793939c264e6f785bca01753d17318d144bd44b407fb3493acaa87", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "d4b91c713e4a784a3f7b1e3cc016eefc619f6b1c3898464222867cafd3c681a3"},
"flow": {:hex, :flow, "0.15.0", "503717c0e367b5713336181d5305106840f64abbad32c75d7af5ef1bb0908e38", [:mix], [{:gen_stage, "~> 0.14.0", [hex: :gen_stage, repo: "hexpm", optional: false]}], "hexpm", "d7ecbd4dd38a188494bc996d5014ef8335f436a0b262140a1f6441ae94714581"},
"gen_stage": {:hex, :gen_stage, "0.14.3", "d0c66f1c87faa301c1a85a809a3ee9097a4264b2edf7644bf5c123237ef732bf", [:mix], [], "hexpm", "8453e2289d94c3199396eb517d65d6715ef26bcae0ee83eb5ff7a84445458d76"},
"gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"},
@ -89,10 +89,10 @@
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"phoenix": {:hex, :phoenix, "1.5.13", "d4e0805ec0973bed80d67302631130fb47d75b1a0b7335a0b23c4432b6ce55ee", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1a7c4f1900e6e60bb60ae6680e48418e3f7c360d58bcb9f812487b6d0d281a0f"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"},
"phoenix_html": {:hex, :phoenix_html, "2.14.3", "51f720d0d543e4e157ff06b65de38e13303d5778a7919bcc696599e5934271b8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "efd697a7fff35a13eeeb6b43db884705cba353a1a41d127d118fda5f90c8e80f"},
"phoenix_html": {:hex, :phoenix_html, "3.0.4", "232d41884fe6a9c42d09f48397c175cd6f0d443aaa34c7424da47604201df2e1", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "ce17fd3cf815b2ed874114073e743507704b1f5288bb03c304a77458485efc8b"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
"plug": {:hex, :plug, "1.12.1", "645678c800601d8d9f27ad1aebba1fdb9ce5b2623ddb961a074da0b96c35187d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d57e799a777bc20494b784966dc5fbda91eb4a09f571f76545b72a634ce0d30b"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
"plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"},
"plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"},
"plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
"poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"},

Loading…
Cancel
Save