From 67a54f3562ee53c4a60a837d251bedac1db30996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Tanr=C4=B1kulu?= Date: Thu, 9 May 2024 15:20:53 +0200 Subject: [PATCH 1/8] discontinue non functioning testnet chains --- README.md | 2 +- docgen.js | 2 +- mock/entry.mock.ts | 6 +++--- src/assets/doc_output.json | 4 +--- src/config.ts | 2 -- src/index.test.ts | 6 +++--- src/service/network.ts | 17 ----------------- src/service/queryNFT.ts | 3 --- 8 files changed, 9 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 1bd161f..70870ce 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ ### Request -- __network:__ Name of the chain to query for. (mainnet | goerli ...) +- __network:__ Name of the chain to query for. (mainnet | sepolia ...) - __contactAddress:__ accepts contractAddress of the NFT which represented by the tokenId - __NFT v1 - tokenId:__ accepts ENS name or labelhash of ENS name in both hex and int format - __NFT v2 - tokenId:__ accepts ENS name or namehash of ENS name in both hex and int format diff --git a/docgen.js b/docgen.js index 0d0adfd..e735447 100644 --- a/docgen.js +++ b/docgen.js @@ -59,7 +59,7 @@ const doc = { tokenId: '4221908525551133525058944220830153...', networkName: { description: 'Name of the chain to query for.', - '@enum': ['mainnet', 'rinkeby', 'ropsten', 'goerli', 'sepolia'], + '@enum': ['mainnet', 'sepolia'], }, }, components: { diff --git a/mock/entry.mock.ts b/mock/entry.mock.ts index d67b185..191b0b8 100644 --- a/mock/entry.mock.ts +++ b/mock/entry.mock.ts @@ -20,7 +20,7 @@ import { WrappedDomainResponse, } from './interface'; -const { SUBGRAPH_URL: subgraph_url } = getNetwork('goerli'); +const { SUBGRAPH_URL: subgraph_url } = getNetwork('sepolia'); const SUBGRAPH_URL = new URL(subgraph_url); const SUBGRAPH_PATH = SUBGRAPH_URL.pathname + SUBGRAPH_URL.search; @@ -111,10 +111,10 @@ export class MockEntry { }); (_metadata as Metadata).setImage( - `https://metadata.ens.domains/goerli/${ADDRESS_NAME_WRAPPER}/${this.namehash}/image` + `https://metadata.ens.domains/sepolia/${ADDRESS_NAME_WRAPPER}/${this.namehash}/image` ); (_metadata as Metadata).setBackground( - `https://metadata.ens.domains/goerli/avatar/${name}` + `https://metadata.ens.domains/sepolia/avatar/${name}` ); this.domainResponse = { diff --git a/src/assets/doc_output.json b/src/assets/doc_output.json index 4951e39..f595821 100644 --- a/src/assets/doc_output.json +++ b/src/assets/doc_output.json @@ -528,9 +528,7 @@ "type": "string", "enum": [ "mainnet", - "rinkeby", - "ropsten", - "goerli" + "sepolia" ], "xml": { "name": "networkName" diff --git a/src/config.ts b/src/config.ts index 0dc9b25..7389b2d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -19,7 +19,6 @@ const NODE_PROVIDER_URL = process.env.NODE_PROVIDER_URL || 'http://localhost:854 // undocumented, temporary keys const NODE_PROVIDER_URL_CF = process.env.NODE_PROVIDER_URL_CF || ''; -const NODE_PROVIDER_URL_GOERLI = process.env.NODE_PROVIDER_URL_GOERLI || ''; const NODE_PROVIDER_URL_SEPOLIA = process.env.NODE_PROVIDER_URL_SEPOLIA || ''; const ADDRESS_ETH_REGISTRAR = process.env.ADDRESS_ETH_REGISTRAR || '0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85'; @@ -56,7 +55,6 @@ export { NODE_PROVIDER, NODE_PROVIDER_URL, NODE_PROVIDER_URL_CF, - NODE_PROVIDER_URL_GOERLI, NODE_PROVIDER_URL_SEPOLIA, RESPONSE_TIMEOUT, SERVER_URL, diff --git a/src/index.test.ts b/src/index.test.ts index 32cc4a9..145bcd6 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -21,7 +21,7 @@ import { GET_DOMAINS } from './service/subgraph'; import { nockProvider, requireUncached } from '../mock/helper'; import { Metadata } from './service/metadata'; -const TEST_NETWORK = 'goerli'; +const TEST_NETWORK = 'sepolia'; const { WEB3_URL: web3_url, SUBGRAPH_URL: subgraph_url } = getNetwork(TEST_NETWORK); @@ -93,7 +93,7 @@ test.before(async (t: ExecutionContext) => { nockProvider(WEB3_URL, 'eth_chainId', [], { id: 1, jsonrpc: '2.0', - result: '0x05', // goerli + result: '0xAA36A7', // sepolia }); nockProvider(WEB3_URL, 'net_version', [], { jsonrpc: '2.0', @@ -452,7 +452,7 @@ test('raise ContractMismatchError', async (t: ExecutionContext) => response: { body }, }: HTTPError = (await t.throwsAsync( () => - got(`goerli/${NON_CONTRACT_ADDRESS}/${sub1Wrappertest.namehash}`, { + got(`sepolia/${NON_CONTRACT_ADDRESS}/${sub1Wrappertest.namehash}`, { ...options, retry: 0, }), diff --git a/src/service/network.ts b/src/service/network.ts index 7cfdfb9..76cb5eb 100644 --- a/src/service/network.ts +++ b/src/service/network.ts @@ -4,7 +4,6 @@ import { NODE_PROVIDER, NODE_PROVIDER_URL, NODE_PROVIDER_URL_CF, - NODE_PROVIDER_URL_GOERLI, NODE_PROVIDER_URL_SEPOLIA, } from '../config'; @@ -17,9 +16,6 @@ const NODE_PROVIDERS = { export const NETWORK = { LOCAL: 'local', - RINKEBY: 'rinkeby', - ROPSTEN: 'ropsten', - GOERLI: 'goerli', SEPOLIA: 'sepolia', MAINNET: 'mainnet', } as const; @@ -38,7 +34,6 @@ function getWeb3URL( return `${api}/${network}`; case NODE_PROVIDERS.GOOGLE: if (network === NETWORK.MAINNET) return api; - if (network === NETWORK.GOERLI) return NODE_PROVIDER_URL_GOERLI; if (network === NETWORK.SEPOLIA) return NODE_PROVIDER_URL_SEPOLIA; return `${NODE_PROVIDER_URL_CF}/${network}`; case NODE_PROVIDERS.GETH: @@ -60,18 +55,6 @@ export default function getNetwork(network: NetworkName): { case NETWORK.LOCAL: SUBGRAPH_URL = 'http://127.0.0.1:8000/subgraphs/name/graphprotocol/ens'; break; - case NETWORK.RINKEBY: - SUBGRAPH_URL = - 'https://api.thegraph.com/subgraphs/name/makoto/ensrinkeby'; - break; - case NETWORK.ROPSTEN: - SUBGRAPH_URL = - 'https://api.thegraph.com/subgraphs/name/ensdomains/ensropsten'; - break; - case NETWORK.GOERLI: - SUBGRAPH_URL = - 'https://api.thegraph.com/subgraphs/name/ensdomains/ensgoerli'; - break; case NETWORK.SEPOLIA: SUBGRAPH_URL = 'https://api.studio.thegraph.com/query/49574/enssepolia/version/latest'; diff --git a/src/service/queryNFT.ts b/src/service/queryNFT.ts index 6ae2e19..8e505db 100644 --- a/src/service/queryNFT.ts +++ b/src/service/queryNFT.ts @@ -4,9 +4,6 @@ import { UnsupportedNetwork } from '../base'; const networks: { [key: string]: string } = { '1': 'mainnet', - '3': 'ropsten', - '4': 'rinkeby', - '5': 'goerli', '11155111': 'sepolia' }; From 8b326edc6cd36de604476f232d6a49878384906d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Tanr=C4=B1kulu?= Date: Tue, 14 May 2024 13:28:25 +0200 Subject: [PATCH 2/8] add xml-parser to detect any svg content, with or without mimetype --- package.json | 2 +- src/service/avatar.ts | 3 ++- src/utils/isSVG.ts | 39 +++++++++++++++++++++++++++++++++++++++ yarn.lock | 2 +- 4 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 src/utils/isSVG.ts diff --git a/package.json b/package.json index cb931ca..3631e53 100644 --- a/package.json +++ b/package.json @@ -21,12 +21,12 @@ "emoji-regex": "^10.1.0", "ethers": "6.12.0", "express": "^4.18.1", + "fast-xml-parser": "^4.3.6", "google-auth-library": "^8.1.0", "graphql": "^16.5.0", "graphql-request": "^4.3.0", "helmet": "^6.1.5", "ioredis": "^5.3.2", - "is-svg": "^4.3.2", "jsdom": "^19.0.0", "lodash": "^4.17.21", "multiformats": "^9.4.8", diff --git a/src/service/avatar.ts b/src/service/avatar.ts index 355438c..1df630e 100644 --- a/src/service/avatar.ts +++ b/src/service/avatar.ts @@ -13,6 +13,7 @@ import { OPENSEA_API_KEY } from '../config'; import { abortableFetch } from '../utils/abortableFetch'; +import isSvg from '../utils/isSVG'; const window = new JSDOM('').window; @@ -91,7 +92,7 @@ export class AvatarMetadata { const mimeType = response?.headers.get('Content-Type'); const data = await response?.buffer(); - if (mimeType?.includes('svg')) { + if (mimeType?.includes('svg') || isSvg(data.toString())) { const DOMPurify = createDOMPurify(window); const cleanData = DOMPurify.sanitize(data.toString()); return [Buffer.from(cleanData), mimeType]; diff --git a/src/utils/isSVG.ts b/src/utils/isSVG.ts new file mode 100644 index 0000000..0d464a3 --- /dev/null +++ b/src/utils/isSVG.ts @@ -0,0 +1,39 @@ +// @ref: https://github.com/sindresorhus/is-svg +// @ref: https://github.com/sindresorhus/is-svg/pull/38 +import {XMLParser, XMLValidator} from 'fast-xml-parser'; + +export default function isSvg(data: string) { + if (typeof data !== 'string') { + throw new TypeError(`Expected a \`string\`, got \`${typeof data}\``); + } + + data = data.toLowerCase().trim(); + + if (data.length === 0) { + return false; + } + + // Has to be `!==` as it can also return an object with error info. + if (XMLValidator.validate(data) !== true) { + return false; + } + + let jsonObject; + const parser = new XMLParser(); + + try { + jsonObject = parser.parse(data); + } catch { + return false; + } + + if (!jsonObject) { + return false; + } + + if (!('svg' in jsonObject)) { + return false; + } + + return true; +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index db77546..916465c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2511,7 +2511,7 @@ fast-text-encoding@^1.0.0: resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867" integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w== -fast-xml-parser@^4.1.3: +fast-xml-parser@^4.1.3, fast-xml-parser@^4.3.6: version "4.3.6" resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.3.6.tgz#190f9d99097f0c8f2d3a0e681a10404afca052ff" integrity sha512-M2SovcRxD4+vC493Uc2GZVcZaj66CCJhWurC4viynVSTvrpErCShNcDz1lAho6n9REQKvL/ll4A4/fw6Y9z8nw== From 5e9c9ce18211b07751884ae7f0a7a5a15c34097f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Tanr=C4=B1kulu?= Date: Fri, 7 Jun 2024 02:03:49 +0200 Subject: [PATCH 3/8] update sanitization --- src/service/avatar.ts | 23 +++++++++++++---------- src/utils/isSvg.ts | 39 +++++++++++++++++++++++++++++++++++++++ src/utils/rateLimiter.ts | 2 +- 3 files changed, 53 insertions(+), 11 deletions(-) create mode 100644 src/utils/isSvg.ts diff --git a/src/service/avatar.ts b/src/service/avatar.ts index 1df630e..f2cfe55 100644 --- a/src/service/avatar.ts +++ b/src/service/avatar.ts @@ -13,7 +13,7 @@ import { OPENSEA_API_KEY } from '../config'; import { abortableFetch } from '../utils/abortableFetch'; -import isSvg from '../utils/isSVG'; +import isSvg from '../utils/isSvg'; const window = new JSDOM('').window; @@ -50,13 +50,11 @@ export class AvatarMetadata { avtResolver: AvatarResolver; constructor(provider: JsonRpcProvider, uri: string) { this.defaultProvider = provider; - this.avtResolver = new AvatarResolver(provider, - { - ipfs: IPFS_GATEWAY, - apiKey: { opensea: OPENSEA_API_KEY }, - urlDenyList: [ 'metadata.ens.domains' ] - } - ); + this.avtResolver = new AvatarResolver(provider, { + ipfs: IPFS_GATEWAY, + apiKey: { opensea: OPENSEA_API_KEY }, + urlDenyList: ['metadata.ens.domains'], + }); this.uri = uri; } @@ -73,7 +71,10 @@ export class AvatarMetadata { if (typeof error === 'string') { console.log(`${this.uri} - error:`, error); } - throw new RetrieveURIFailed(`Error fetching avatar: Provided url or NFT source is broken.`, 404); + throw new RetrieveURIFailed( + `Error fetching avatar: Provided url or NFT source is broken.`, + 404 + ); } if (!avatarURI) { @@ -94,7 +95,9 @@ export class AvatarMetadata { if (mimeType?.includes('svg') || isSvg(data.toString())) { const DOMPurify = createDOMPurify(window); - const cleanData = DOMPurify.sanitize(data.toString()); + const cleanData = DOMPurify.sanitize(data.toString(), { + FORBID_TAGS: ['a', 'area', 'base', 'iframe', 'link'], + }); return [Buffer.from(cleanData), mimeType]; } diff --git a/src/utils/isSvg.ts b/src/utils/isSvg.ts new file mode 100644 index 0000000..0d464a3 --- /dev/null +++ b/src/utils/isSvg.ts @@ -0,0 +1,39 @@ +// @ref: https://github.com/sindresorhus/is-svg +// @ref: https://github.com/sindresorhus/is-svg/pull/38 +import {XMLParser, XMLValidator} from 'fast-xml-parser'; + +export default function isSvg(data: string) { + if (typeof data !== 'string') { + throw new TypeError(`Expected a \`string\`, got \`${typeof data}\``); + } + + data = data.toLowerCase().trim(); + + if (data.length === 0) { + return false; + } + + // Has to be `!==` as it can also return an object with error info. + if (XMLValidator.validate(data) !== true) { + return false; + } + + let jsonObject; + const parser = new XMLParser(); + + try { + jsonObject = parser.parse(data); + } catch { + return false; + } + + if (!jsonObject) { + return false; + } + + if (!('svg' in jsonObject)) { + return false; + } + + return true; +} \ No newline at end of file diff --git a/src/utils/rateLimiter.ts b/src/utils/rateLimiter.ts index cc0ac39..e2a1ee6 100644 --- a/src/utils/rateLimiter.ts +++ b/src/utils/rateLimiter.ts @@ -18,7 +18,7 @@ if (REDIS_URL) { const opts = { storeClient: redisClient, - points: 20, // Number of total points + points: 40, // Number of total points duration: 2, // Per second(s) execEvenly: false, // Do not delay actions evenly blockDuration: 0, // Do not block the caller if consumed more than points From 859b5b2695d6fb97043a3f56e167f3b50b4902ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Tanr=C4=B1kulu?= Date: Tue, 14 May 2024 13:28:25 +0200 Subject: [PATCH 4/8] add xml-parser to detect any svg content, with or without mimetype --- package.json | 2 +- src/service/avatar.ts | 3 ++- src/utils/isSVG.ts | 39 +++++++++++++++++++++++++++++++++++++++ yarn.lock | 2 +- 4 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 src/utils/isSVG.ts diff --git a/package.json b/package.json index cb931ca..3631e53 100644 --- a/package.json +++ b/package.json @@ -21,12 +21,12 @@ "emoji-regex": "^10.1.0", "ethers": "6.12.0", "express": "^4.18.1", + "fast-xml-parser": "^4.3.6", "google-auth-library": "^8.1.0", "graphql": "^16.5.0", "graphql-request": "^4.3.0", "helmet": "^6.1.5", "ioredis": "^5.3.2", - "is-svg": "^4.3.2", "jsdom": "^19.0.0", "lodash": "^4.17.21", "multiformats": "^9.4.8", diff --git a/src/service/avatar.ts b/src/service/avatar.ts index 355438c..1df630e 100644 --- a/src/service/avatar.ts +++ b/src/service/avatar.ts @@ -13,6 +13,7 @@ import { OPENSEA_API_KEY } from '../config'; import { abortableFetch } from '../utils/abortableFetch'; +import isSvg from '../utils/isSVG'; const window = new JSDOM('').window; @@ -91,7 +92,7 @@ export class AvatarMetadata { const mimeType = response?.headers.get('Content-Type'); const data = await response?.buffer(); - if (mimeType?.includes('svg')) { + if (mimeType?.includes('svg') || isSvg(data.toString())) { const DOMPurify = createDOMPurify(window); const cleanData = DOMPurify.sanitize(data.toString()); return [Buffer.from(cleanData), mimeType]; diff --git a/src/utils/isSVG.ts b/src/utils/isSVG.ts new file mode 100644 index 0000000..0d464a3 --- /dev/null +++ b/src/utils/isSVG.ts @@ -0,0 +1,39 @@ +// @ref: https://github.com/sindresorhus/is-svg +// @ref: https://github.com/sindresorhus/is-svg/pull/38 +import {XMLParser, XMLValidator} from 'fast-xml-parser'; + +export default function isSvg(data: string) { + if (typeof data !== 'string') { + throw new TypeError(`Expected a \`string\`, got \`${typeof data}\``); + } + + data = data.toLowerCase().trim(); + + if (data.length === 0) { + return false; + } + + // Has to be `!==` as it can also return an object with error info. + if (XMLValidator.validate(data) !== true) { + return false; + } + + let jsonObject; + const parser = new XMLParser(); + + try { + jsonObject = parser.parse(data); + } catch { + return false; + } + + if (!jsonObject) { + return false; + } + + if (!('svg' in jsonObject)) { + return false; + } + + return true; +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index db77546..916465c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2511,7 +2511,7 @@ fast-text-encoding@^1.0.0: resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867" integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w== -fast-xml-parser@^4.1.3: +fast-xml-parser@^4.1.3, fast-xml-parser@^4.3.6: version "4.3.6" resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.3.6.tgz#190f9d99097f0c8f2d3a0e681a10404afca052ff" integrity sha512-M2SovcRxD4+vC493Uc2GZVcZaj66CCJhWurC4viynVSTvrpErCShNcDz1lAho6n9REQKvL/ll4A4/fw6Y9z8nw== From eaa1a1959018c8eb8f46bdf3c72966f51cc6b786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Tanr=C4=B1kulu?= Date: Fri, 7 Jun 2024 02:03:49 +0200 Subject: [PATCH 5/8] update sanitization --- src/service/avatar.ts | 23 +++++++++++++---------- src/utils/isSvg.ts | 39 +++++++++++++++++++++++++++++++++++++++ src/utils/rateLimiter.ts | 2 +- 3 files changed, 53 insertions(+), 11 deletions(-) create mode 100644 src/utils/isSvg.ts diff --git a/src/service/avatar.ts b/src/service/avatar.ts index 1df630e..f2cfe55 100644 --- a/src/service/avatar.ts +++ b/src/service/avatar.ts @@ -13,7 +13,7 @@ import { OPENSEA_API_KEY } from '../config'; import { abortableFetch } from '../utils/abortableFetch'; -import isSvg from '../utils/isSVG'; +import isSvg from '../utils/isSvg'; const window = new JSDOM('').window; @@ -50,13 +50,11 @@ export class AvatarMetadata { avtResolver: AvatarResolver; constructor(provider: JsonRpcProvider, uri: string) { this.defaultProvider = provider; - this.avtResolver = new AvatarResolver(provider, - { - ipfs: IPFS_GATEWAY, - apiKey: { opensea: OPENSEA_API_KEY }, - urlDenyList: [ 'metadata.ens.domains' ] - } - ); + this.avtResolver = new AvatarResolver(provider, { + ipfs: IPFS_GATEWAY, + apiKey: { opensea: OPENSEA_API_KEY }, + urlDenyList: ['metadata.ens.domains'], + }); this.uri = uri; } @@ -73,7 +71,10 @@ export class AvatarMetadata { if (typeof error === 'string') { console.log(`${this.uri} - error:`, error); } - throw new RetrieveURIFailed(`Error fetching avatar: Provided url or NFT source is broken.`, 404); + throw new RetrieveURIFailed( + `Error fetching avatar: Provided url or NFT source is broken.`, + 404 + ); } if (!avatarURI) { @@ -94,7 +95,9 @@ export class AvatarMetadata { if (mimeType?.includes('svg') || isSvg(data.toString())) { const DOMPurify = createDOMPurify(window); - const cleanData = DOMPurify.sanitize(data.toString()); + const cleanData = DOMPurify.sanitize(data.toString(), { + FORBID_TAGS: ['a', 'area', 'base', 'iframe', 'link'], + }); return [Buffer.from(cleanData), mimeType]; } diff --git a/src/utils/isSvg.ts b/src/utils/isSvg.ts new file mode 100644 index 0000000..0d464a3 --- /dev/null +++ b/src/utils/isSvg.ts @@ -0,0 +1,39 @@ +// @ref: https://github.com/sindresorhus/is-svg +// @ref: https://github.com/sindresorhus/is-svg/pull/38 +import {XMLParser, XMLValidator} from 'fast-xml-parser'; + +export default function isSvg(data: string) { + if (typeof data !== 'string') { + throw new TypeError(`Expected a \`string\`, got \`${typeof data}\``); + } + + data = data.toLowerCase().trim(); + + if (data.length === 0) { + return false; + } + + // Has to be `!==` as it can also return an object with error info. + if (XMLValidator.validate(data) !== true) { + return false; + } + + let jsonObject; + const parser = new XMLParser(); + + try { + jsonObject = parser.parse(data); + } catch { + return false; + } + + if (!jsonObject) { + return false; + } + + if (!('svg' in jsonObject)) { + return false; + } + + return true; +} \ No newline at end of file diff --git a/src/utils/rateLimiter.ts b/src/utils/rateLimiter.ts index cc0ac39..e2a1ee6 100644 --- a/src/utils/rateLimiter.ts +++ b/src/utils/rateLimiter.ts @@ -18,7 +18,7 @@ if (REDIS_URL) { const opts = { storeClient: redisClient, - points: 20, // Number of total points + points: 40, // Number of total points duration: 2, // Per second(s) execEvenly: false, // Do not delay actions evenly blockDuration: 0, // Do not block the caller if consumed more than points From a774e2a5e5e020ce374d886d07104085284ea721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Tanr=C4=B1kulu?= Date: Thu, 4 Jul 2024 15:02:40 +0200 Subject: [PATCH 6/8] update ens-avatar, add ssrf filter for each reqs, update docs --- package.json | 2 +- src/assets/doc_output.json | 7 +++++-- src/controller/queryNFT.ts | 6 ++++++ src/service/avatar.ts | 38 ++++++++++++++++++++++++------------- src/service/network.ts | 4 +++- src/service/queryNFT.ts | 13 ++++++++++++- src/utils/abortableFetch.ts | 2 +- tsconfig.json | 3 ++- yarn.lock | 8 ++++---- 9 files changed, 59 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index 3631e53..ab97db6 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "dependencies": { "@adraffy/ens-normalize": "^1.10.0", - "@ensdomains/ens-avatar": "^1.0.0-alpha.1.ethers.6", + "@ensdomains/ens-avatar": "^1.0.0-alpha.2.ethers.6", "@ensdomains/ensjs": "^3.7.0", "@types/lodash": "^4.14.170", "btoa": "^1.2.1", diff --git a/src/assets/doc_output.json b/src/assets/doc_output.json index 4951e39..893d4cb 100644 --- a/src/assets/doc_output.json +++ b/src/assets/doc_output.json @@ -3,9 +3,12 @@ "info": { "version": "0.0.1-alpha.1", "title": "ENS Metadata Service", - "description": "Set of endpoints to query ENS metadata and more", + "description": "Set of endpoints to query ENS metadata and more.

Important: While we have taken extensive measures to sanitize the user provided content, developers are advised to exercise caution when interacting with this API endpoint. In case of any advise or question, please reach us.", "contact": "contact@ens.domains", - "license": "MIT License", + "license": { + "name": "MIT", + "url": "https://opensource.org/license/mit" + }, "x-logo": { "url": "/assets/logo.svg", "backgroundColor": "#FFFFFF", diff --git a/src/controller/queryNFT.ts b/src/controller/queryNFT.ts index 8d4d1ba..e5fcc9d 100644 --- a/src/controller/queryNFT.ts +++ b/src/controller/queryNFT.ts @@ -18,6 +18,12 @@ export async function queryNFTep(req: Request, res: Response) { } */ res.status(200).json(metadata); } catch (error) { + if (error.code === 'ERR_FR_REDIRECTION_FAILURE') { + res.status(500).json({ + message: "Redirection is not allowed", + }); + return; + } /* #swagger.responses[500] = { description: 'Internal Server Error' } */ diff --git a/src/service/avatar.ts b/src/service/avatar.ts index f2cfe55..db2e8f9 100644 --- a/src/service/avatar.ts +++ b/src/service/avatar.ts @@ -1,19 +1,21 @@ -import { AvatarResolver } from '@ensdomains/ens-avatar'; -import { strict as assert } from 'assert'; -import { ethers, JsonRpcProvider } from 'ethers'; -import createDOMPurify from 'dompurify'; -import { JSDOM } from 'jsdom'; +import http from 'http'; +import https from 'https'; + +import { AvatarResolver } from '@ensdomains/ens-avatar'; +import { strict as assert } from 'assert'; +import { JsonRpcProvider } from 'ethers'; +import createDOMPurify from 'dompurify'; +import { JSDOM } from 'jsdom'; import { ResolverNotFound, RetrieveURIFailed, TextRecordNotFound, -} from '../base'; -import { - IPFS_GATEWAY, - OPENSEA_API_KEY -} from '../config'; -import { abortableFetch } from '../utils/abortableFetch'; -import isSvg from '../utils/isSvg'; +} from '../base'; +import { IPFS_GATEWAY, OPENSEA_API_KEY } from '../config'; +import { abortableFetch } from '../utils/abortableFetch'; +import isSvg from '../utils/isSvg'; + +const { requestFilterHandler } = require('ssrf-req-filter'); const window = new JSDOM('').window; @@ -54,6 +56,10 @@ export class AvatarMetadata { ipfs: IPFS_GATEWAY, apiKey: { opensea: OPENSEA_API_KEY }, urlDenyList: ['metadata.ens.domains'], + agents: { + httpAgent: requestFilterHandler(new http.Agent()), + httpsAgent: requestFilterHandler(new https.Agent()), + }, }); this.uri = uri; } @@ -86,7 +92,13 @@ export class AvatarMetadata { if (avatarURI?.startsWith('http')) { // abort fetching image after 5sec - const response = await abortableFetch(avatarURI, { timeout: 7000 }); + const response = await abortableFetch(avatarURI, { + timeout: 7000, + headers: { + 'user-agent': 'ENS-ImageFetcher/1.0.0', + }, + }) + assert(!!response, 'Response is empty'); diff --git a/src/service/network.ts b/src/service/network.ts index 5ba6c5d..2ddd188 100644 --- a/src/service/network.ts +++ b/src/service/network.ts @@ -36,7 +36,9 @@ function getWeb3URL( case NODE_PROVIDERS.INFURA: return `${api.replace('https://', `https://${network}.`)}`; case NODE_PROVIDERS.CLOUDFLARE: - return `${api}/${network}`; + if (network === NETWORK.MAINNET) return `${api}/${network}`; + if (network === NETWORK.GOERLI) return NODE_PROVIDER_URL_GOERLI; + if (network === NETWORK.SEPOLIA) return NODE_PROVIDER_URL_SEPOLIA; case NODE_PROVIDERS.GOOGLE: if (network === NETWORK.MAINNET) return api; if (network === NETWORK.GOERLI) return NODE_PROVIDER_URL_GOERLI; diff --git a/src/service/queryNFT.ts b/src/service/queryNFT.ts index 6ae2e19..5e4d449 100644 --- a/src/service/queryNFT.ts +++ b/src/service/queryNFT.ts @@ -1,7 +1,12 @@ +import http from 'http'; +import https from 'https'; + import { utils, specs, UnsupportedNamespace } from '@ensdomains/ens-avatar'; import getNetwork, { NetworkName } from '../service/network'; import { UnsupportedNetwork } from '../base'; +const { requestFilterHandler } = require('ssrf-req-filter'); + const networks: { [key: string]: string } = { '1': 'mainnet', '3': 'ropsten', @@ -38,7 +43,13 @@ export async function queryNFT(uri: string) { provider, undefined, contractAddress, - tokenID + tokenID, + { + agents: { + httpAgent: requestFilterHandler(new http.Agent()), + httpsAgent: requestFilterHandler(new https.Agent()) + } + } ); return { host_meta, ...metadata }; } diff --git a/src/utils/abortableFetch.ts b/src/utils/abortableFetch.ts index 3d379bd..e6ec048 100644 --- a/src/utils/abortableFetch.ts +++ b/src/utils/abortableFetch.ts @@ -5,7 +5,7 @@ import timeoutSignal from 'timeout-signal'; const ssrfFilter = require('ssrf-req-filter'); interface AbortableFetchOpts { - timeout?: number; + [key: string]: any; } export async function abortableFetch( diff --git a/tsconfig.json b/tsconfig.json index ea1f615..acd6678 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -68,7 +68,8 @@ /* Advanced Options */ "resolveJsonModule": true, "skipLibCheck": true, /* Skip type checking of declaration files. */ - "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ + "useUnknownInCatchVariables": false }, "exclude": ["./src/**/*.test.ts", "./mock/**/*"] } diff --git a/yarn.lock b/yarn.lock index 916465c..8bdfe66 100644 --- a/yarn.lock +++ b/yarn.lock @@ -250,10 +250,10 @@ dns-packet "^5.6.1" typescript-logging "^1.0.1" -"@ensdomains/ens-avatar@^1.0.0-alpha.1.ethers.6": - version "1.0.0-alpha.1.ethers.6" - resolved "https://registry.yarnpkg.com/@ensdomains/ens-avatar/-/ens-avatar-1.0.0-alpha.1.ethers.6.tgz#01829c09803b26f838cf1a78714e9f8100c710ce" - integrity sha512-CwkGDegpEZ13WBxK3LNfJUL5cczQHF9Ar8L9faEXLXIh4OkvEZWwpYNEB9Fn/UhYkS8/R4sGtx4ImqfppNujCA== +"@ensdomains/ens-avatar@^1.0.0-alpha.2.ethers.6": + version "1.0.0-alpha.2.ethers.6" + resolved "https://registry.yarnpkg.com/@ensdomains/ens-avatar/-/ens-avatar-1.0.0-alpha.2.ethers.6.tgz#b406c20fec6fa98e3427e92970686c8cdafbddeb" + integrity sha512-lx8Ggmp6uLPQHzgunMvVUG3MfMLhyhSoY03Fb5HyjAK39OjYl7pMB+nmWnh+l4foKztMqXBfXLJ9WOYls5AyAw== dependencies: "@ethersproject/contracts" "^5.7.0" "@ethersproject/providers" "^5.7.0" From 82897c13e82f5240a7eeec44cc6c01eb65eac4e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Tanr=C4=B1kulu?= Date: Fri, 5 Jul 2024 19:03:29 +0200 Subject: [PATCH 7/8] EOF --- src/utils/isSvg.ts | 54 +++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/utils/isSvg.ts b/src/utils/isSvg.ts index 0d464a3..01e8657 100644 --- a/src/utils/isSvg.ts +++ b/src/utils/isSvg.ts @@ -1,39 +1,39 @@ // @ref: https://github.com/sindresorhus/is-svg // @ref: https://github.com/sindresorhus/is-svg/pull/38 -import {XMLParser, XMLValidator} from 'fast-xml-parser'; +import { XMLParser, XMLValidator } from 'fast-xml-parser'; export default function isSvg(data: string) { - if (typeof data !== 'string') { - throw new TypeError(`Expected a \`string\`, got \`${typeof data}\``); - } + if (typeof data !== 'string') { + throw new TypeError(`Expected a \`string\`, got \`${typeof data}\``); + } - data = data.toLowerCase().trim(); + data = data.toLowerCase().trim(); - if (data.length === 0) { - return false; - } + if (data.length === 0) { + return false; + } - // Has to be `!==` as it can also return an object with error info. - if (XMLValidator.validate(data) !== true) { - return false; - } + // Has to be `!==` as it can also return an object with error info. + if (XMLValidator.validate(data) !== true) { + return false; + } - let jsonObject; - const parser = new XMLParser(); + let jsonObject; + const parser = new XMLParser(); - try { - jsonObject = parser.parse(data); - } catch { - return false; - } + try { + jsonObject = parser.parse(data); + } catch { + return false; + } - if (!jsonObject) { - return false; - } + if (!jsonObject) { + return false; + } - if (!('svg' in jsonObject)) { - return false; - } + if (!('svg' in jsonObject)) { + return false; + } - return true; -} \ No newline at end of file + return true; +} From 84178dde1d298be4f218533de1986932dac72df3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Tanr=C4=B1kulu?= Date: Fri, 5 Jul 2024 19:04:42 +0200 Subject: [PATCH 8/8] Delete src/utils/isSVG.ts --- src/utils/isSVG.ts | 39 --------------------------------------- 1 file changed, 39 deletions(-) delete mode 100644 src/utils/isSVG.ts diff --git a/src/utils/isSVG.ts b/src/utils/isSVG.ts deleted file mode 100644 index 0d464a3..0000000 --- a/src/utils/isSVG.ts +++ /dev/null @@ -1,39 +0,0 @@ -// @ref: https://github.com/sindresorhus/is-svg -// @ref: https://github.com/sindresorhus/is-svg/pull/38 -import {XMLParser, XMLValidator} from 'fast-xml-parser'; - -export default function isSvg(data: string) { - if (typeof data !== 'string') { - throw new TypeError(`Expected a \`string\`, got \`${typeof data}\``); - } - - data = data.toLowerCase().trim(); - - if (data.length === 0) { - return false; - } - - // Has to be `!==` as it can also return an object with error info. - if (XMLValidator.validate(data) !== true) { - return false; - } - - let jsonObject; - const parser = new XMLParser(); - - try { - jsonObject = parser.parse(data); - } catch { - return false; - } - - if (!jsonObject) { - return false; - } - - if (!('svg' in jsonObject)) { - return false; - } - - return true; -} \ No newline at end of file