From d24d221550ef40d4dc3b4ee22b935408abb84429 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 14 Aug 2024 15:32:25 +0200 Subject: [PATCH] Add SSRF protection --- config/default.yaml | 4 + config/production.yaml.example | 4 + config/test.yaml | 3 + package.json | 1 + packages/tests/src/api/server/index.ts | 1 + packages/tests/src/api/server/ssrf.ts | 84 +++++++++++++++++++ .../src/shared/mock-servers/mock-http.ts | 5 ++ .../api/search/search-video-channels.ts | 4 +- .../api/search/search-video-playlists.ts | 4 +- .../controllers/api/search/search-videos.ts | 4 +- server/core/helpers/geo-ip.ts | 4 +- server/core/helpers/requests.ts | 30 +++++-- .../core/helpers/youtube-dl/youtube-dl-cli.ts | 6 +- .../core/initializers/checker-before-init.ts | 2 +- server/core/initializers/config.ts | 1 + .../job-queue/handlers/activitypub-follow.ts | 2 +- .../core/lib/object-storage/shared/client.ts | 4 +- server/core/lib/plugins/plugin-index.ts | 26 +++--- .../schedulers/auto-follow-index-instances.ts | 2 +- .../peertube-version-check-scheduler.ts | 2 +- yarn.lock | 10 ++- 21 files changed, 160 insertions(+), 43 deletions(-) create mode 100644 packages/tests/src/api/server/ssrf.ts diff --git a/config/default.yaml b/config/default.yaml index ada229a21..a2f3450d2 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -436,6 +436,10 @@ federation: # Enable ActivityPub endpoints (inbox/outbox) enabled: true + # Prevent SSRF requests (requests to your internal network for example) by checking the request IP address + # More information about SSRF: https://portswigger.net/web-security/ssrf + prevent_ssrf: true + # Some federated software such as Mastodon may require an HTTP signature to access content sign_federated_fetches: true diff --git a/config/production.yaml.example b/config/production.yaml.example index 50d0db300..4c74a5fdf 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example @@ -434,6 +434,10 @@ federation: # Enable ActivityPub endpoints (inbox/outbox) enabled: true + # Prevent SSRF requests (requests to your internal network for example) by checking the request IP address + # More information about SSRF: https://portswigger.net/web-security/ssrf + prevent_ssrf: true + # Some federated software such as Mastodon may require an HTTP signature to access content sign_federated_fetches: true diff --git a/config/test.yaml b/config/test.yaml index b174e0b18..c5a6b0cf5 100644 --- a/config/test.yaml +++ b/config/test.yaml @@ -140,6 +140,9 @@ plugins: check_latest_versions_interval: '10 minutes' federation: + # We use internal IP address in PeerTube tests + prevent_ssrf: false + videos: federate_unlisted: true cleanup_remote_interactions: false diff --git a/package.json b/package.json index c3a09d770..804e7d4c0 100644 --- a/package.json +++ b/package.json @@ -136,6 +136,7 @@ "fluent-ffmpeg": "^2.1.0", "fs-extra": "^11.1.0", "got": "^13.0.0", + "got-ssrf": "^3.0.0", "helmet": "^7.0.0", "hpagent": "^1.0.0", "http-problem-details": "^0.1.5", diff --git a/packages/tests/src/api/server/index.ts b/packages/tests/src/api/server/index.ts index 5c80a5a37..b7d654500 100644 --- a/packages/tests/src/api/server/index.ts +++ b/packages/tests/src/api/server/index.ts @@ -14,6 +14,7 @@ import './logs.js' import './reverse-proxy.js' import './services.js' import './slow-follows.js' +import './ssrf.js' import './stats.js' import './tracker.js' import './no-client.js' diff --git a/packages/tests/src/api/server/ssrf.ts b/packages/tests/src/api/server/ssrf.ts new file mode 100644 index 000000000..8896d5d70 --- /dev/null +++ b/packages/tests/src/api/server/ssrf.ts @@ -0,0 +1,84 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { + cleanupTests, + createMultipleServers, + doubleFollow, + PeerTubeServer, + setAccessTokensToServers, + waitJobs +} from '@peertube/peertube-server-commands' +import { MockHTTP } from '@tests/shared/mock-servers/mock-http.js' +import { expect } from 'chai' +import { HttpStatusCode } from '../../../../models/src/http/http-status-codes.js' + +describe('Test SSRF requests', function () { + let servers: PeerTubeServer[] = [] + + let baseServerConfig: any + + before(async function () { + this.timeout(240000) + + const mockHTTP = new MockHTTP() + const port = await mockHTTP.initialize() + + baseServerConfig = { + plugins: { + index: { + url: `http://127.0.0.1:${port}/redirect/https://packages.joinpeertube.org` + } + } + } + + servers = await createMultipleServers(2, baseServerConfig) + await setAccessTokensToServers(servers) + + await doubleFollow(servers[0], servers[1]) + }) + + describe('Disabled SSRF protection', function () { + + it('Should not forbid non-unicast federation', async function () { + const { uuid } = await servers[0].videos.quickUpload({ name: 'video' }) + await waitJobs(servers) + + await servers[1].videos.get({ id: uuid }) + }) + + it('Should fetch plugin index', async function () { + const { total } = await servers[0].plugins.listAvailable({ count: 10 }) + + expect(total).to.be.at.least(15) + }) + }) + + describe('Enabled SSRF protection', function () { + before(async function () { + await servers[0].kill() + + await servers[0].run({ + ...baseServerConfig, + + federation: { prevent_ssrf: true } + }) + }) + + it('Should forbid non-unicast federation', async function () { + const { uuid } = await servers[0].videos.quickUpload({ name: 'video' }) + await waitJobs(servers) + + await servers[1].videos.get({ id: uuid, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should still allow plugin index search on internal network', async function () { + const { total } = await servers[0].plugins.listAvailable({ count: 10 }) + + expect(total).to.be.at.least(15) + }) + }) + + after(async function () { + await cleanupTests(servers) + }) +}) diff --git a/packages/tests/src/shared/mock-servers/mock-http.ts b/packages/tests/src/shared/mock-servers/mock-http.ts index bc1a9ce91..70efc95a4 100644 --- a/packages/tests/src/shared/mock-servers/mock-http.ts +++ b/packages/tests/src/shared/mock-servers/mock-http.ts @@ -8,6 +8,11 @@ export class MockHTTP { async initialize () { const app = express() + app.get('/redirect/*', (req: express.Request, res: express.Response, next: express.NextFunction) => { + + return res.redirect(req.params[0]) + }) + app.get('/*', (req: express.Request, res: express.Response, next: express.NextFunction) => { return res.sendStatus(200) }) diff --git a/server/core/controllers/api/search/search-video-channels.ts b/server/core/controllers/api/search/search-video-channels.ts index a278a93dc..920b1a07a 100644 --- a/server/core/controllers/api/search/search-video-channels.ts +++ b/server/core/controllers/api/search/search-video-channels.ts @@ -78,8 +78,8 @@ async function searchVideoChannelsIndex (query: VideoChannelsSearchQueryAfterSan try { logger.debug('Doing video channels search index request on %s.', url, { body }) - const { body: searchIndexResult } = await doJSONRequest>(url, { method: 'POST', json: body }) - const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.video-channels.index.list.result') + const searchIndexResult = await doJSONRequest>(url, { method: 'POST', json: body, preventSSRF: false }) + const jsonResult = await Hooks.wrapObject(searchIndexResult.body, 'filter:api.search.video-channels.index.list.result') return res.json(jsonResult) } catch (err) { diff --git a/server/core/controllers/api/search/search-video-playlists.ts b/server/core/controllers/api/search/search-video-playlists.ts index ca8c90956..f1e34fb6f 100644 --- a/server/core/controllers/api/search/search-video-playlists.ts +++ b/server/core/controllers/api/search/search-video-playlists.ts @@ -69,8 +69,8 @@ async function searchVideoPlaylistsIndex (query: VideoPlaylistsSearchQueryAfterS try { logger.debug('Doing video playlists search index request on %s.', url, { body }) - const { body: searchIndexResult } = await doJSONRequest>(url, { method: 'POST', json: body }) - const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.video-playlists.index.list.result') + const searchIndexResult = await doJSONRequest>(url, { method: 'POST', json: body, preventSSRF: false }) + const jsonResult = await Hooks.wrapObject(searchIndexResult.body, 'filter:api.search.video-playlists.index.list.result') return res.json(jsonResult) } catch (err) { diff --git a/server/core/controllers/api/search/search-videos.ts b/server/core/controllers/api/search/search-videos.ts index 5e929b3c9..2b3d5c4da 100644 --- a/server/core/controllers/api/search/search-videos.ts +++ b/server/core/controllers/api/search/search-videos.ts @@ -87,8 +87,8 @@ async function searchVideosIndex (query: VideosSearchQueryAfterSanitize, res: ex try { logger.debug('Doing videos search index request on %s.', url, { body }) - const { body: searchIndexResult } = await doJSONRequest>(url, { method: 'POST', json: body }) - const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.videos.index.list.result') + const searchIndexResult = await doJSONRequest>(url, { method: 'POST', json: body, preventSSRF: false }) + const jsonResult = await Hooks.wrapObject(searchIndexResult.body, 'filter:api.search.videos.index.list.result') return res.json(jsonResult) } catch (err) { diff --git a/server/core/helpers/geo-ip.ts b/server/core/helpers/geo-ip.ts index 8e2351e53..6f5b9e5cd 100644 --- a/server/core/helpers/geo-ip.ts +++ b/server/core/helpers/geo-ip.ts @@ -6,7 +6,7 @@ import maxmind, { CityResponse, CountryResponse, Reader } from 'maxmind' import { join } from 'path' import { isArray } from './custom-validators/misc.js' import { logger, loggerTagsFactory } from './logger.js' -import { isBinaryResponse, peertubeGot } from './requests.js' +import { isBinaryResponse, unsafeSSRFGot } from './requests.js' const lTags = loggerTagsFactory('geo-ip') @@ -95,7 +95,7 @@ export class GeoIP { const gotOptions = { context: { bodyKBLimit: 800_000 }, responseType: 'buffer' as 'buffer' } try { - const gotResult = await peertubeGot(url, gotOptions) + const gotResult = await unsafeSSRFGot(url, gotOptions) if (!isBinaryResponse(gotResult)) { throw new Error('Not a binary response') diff --git a/server/core/helpers/requests.ts b/server/core/helpers/requests.ts index a0b30e802..ced0b4bdc 100644 --- a/server/core/helpers/requests.ts +++ b/server/core/helpers/requests.ts @@ -1,7 +1,9 @@ import httpSignature from '@peertube/http-signature' +import { CONFIG } from '@server/initializers/config.js' import { createWriteStream } from 'fs' import { remove } from 'fs-extra/esm' import got, { CancelableRequest, OptionsInit, OptionsOfTextResponseBody, OptionsOfUnknownResponseBody, RequestError, Response } from 'got' +import { gotSsrf } from 'got-ssrf' import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent' import { ACTIVITY_PUB, BINARY_CONTENT_TYPES, PEERTUBE_VERSION, REQUEST_TIMEOUTS, WEBSERVER } from '../initializers/constants.js' import { pipelinePromise } from './core-utils.js' @@ -35,8 +37,8 @@ export type PeerTubeRequestOptions = { followRedirect?: boolean } & Pick -export const peertubeGot = got.extend({ - ...getAgent(), +export const unsafeSSRFGot = got.extend({ + ...getProxyAgent(), headers: { 'user-agent': getUserAgent() @@ -116,6 +118,12 @@ export const peertubeGot = got.extend({ } }) +export const peertubeGot = CONFIG.FEDERATION.PREVENT_SSRF + ? got.extend(gotSsrf, unsafeSSRFGot) + : unsafeSSRFGot + +// --------------------------------------------------------------------------- + export function doRequest (url: string, options: PeerTubeRequestOptions = {}) { const gotOptions = buildGotOptions(options) as OptionsOfTextResponseBody @@ -123,10 +131,14 @@ export function doRequest (url: string, options: PeerTubeRequestOptions = {}) { .catch(err => { throw buildRequestError(err) }) } -export function doJSONRequest (url: string, options: PeerTubeRequestOptions = {}) { +export function doJSONRequest (url: string, options: PeerTubeRequestOptions & { preventSSRF?: false } = {}) { const gotOptions = buildGotOptions(options) - return peertubeGot(url, { ...gotOptions, responseType: 'json' }) + const gotInstance = options.preventSSRF === false + ? unsafeSSRFGot + : peertubeGot + + return gotInstance(url, { ...gotOptions, responseType: 'json' }) .catch(err => { throw buildRequestError(err) }) } @@ -154,7 +166,7 @@ export function generateRequestStream (url: string, options: PeerTubeRequestOpti return peertubeGot.stream(url, { ...gotOptions, isStream: true }) } -export function getAgent () { +export function getProxyAgent () { if (!isProxyEnabled()) return {} const proxy = getProxy() @@ -178,10 +190,6 @@ export function getAgent () { } } -export function getUserAgent () { - return `PeerTube/${PEERTUBE_VERSION} (+${WEBSERVER.URL})` -} - export function isBinaryResponse (result: Response) { return BINARY_CONTENT_TYPES.has(result.headers['content-type']) } @@ -190,6 +198,10 @@ export function isBinaryResponse (result: Response) { // Private // --------------------------------------------------------------------------- +function getUserAgent () { + return `PeerTube/${PEERTUBE_VERSION} (+${WEBSERVER.URL})` +} + function buildGotOptions (options: PeerTubeRequestOptions): OptionsOfUnknownResponseBody { const { activityPub, bodyKBLimit = 3000 } = options diff --git a/server/core/helpers/youtube-dl/youtube-dl-cli.ts b/server/core/helpers/youtube-dl/youtube-dl-cli.ts index 6e465600d..fb8db720a 100644 --- a/server/core/helpers/youtube-dl/youtube-dl-cli.ts +++ b/server/core/helpers/youtube-dl/youtube-dl-cli.ts @@ -8,7 +8,7 @@ import { OptionsOfBufferResponseBody } from 'got' import { dirname, join } from 'path' import { logger, loggerTagsFactory } from '../logger.js' import { getProxy, isProxyEnabled } from '../proxy.js' -import { isBinaryResponse, peertubeGot } from '../requests.js' +import { isBinaryResponse, unsafeSSRFGot } from '../requests.js' type ProcessOptions = Pick @@ -45,7 +45,7 @@ export class YoutubeDLCLI { } try { - let gotResult = await peertubeGot(url, gotOptions) + let gotResult = await unsafeSSRFGot(url, gotOptions) if (!isBinaryResponse(gotResult)) { const json = JSON.parse(gotResult.body.toString()) @@ -56,7 +56,7 @@ export class YoutubeDLCLI { const releaseAsset = latest.assets.find(a => a.name === releaseName) if (!releaseAsset) throw new Error(`Cannot find appropriate release with name ${releaseName} in release assets`) - gotResult = await peertubeGot(releaseAsset.browser_download_url, gotOptions) + gotResult = await unsafeSSRFGot(releaseAsset.browser_download_url, gotOptions) } if (!isBinaryResponse(gotResult)) { diff --git a/server/core/initializers/checker-before-init.ts b/server/core/initializers/checker-before-init.ts index 748d74580..6f4c9e173 100644 --- a/server/core/initializers/checker-before-init.ts +++ b/server/core/initializers/checker-before-init.ts @@ -74,7 +74,7 @@ function checkMissedConfig () { 'feeds.videos.count', 'feeds.comments.count', 'geo_ip.enabled', 'geo_ip.country.database_url', 'geo_ip.city.database_url', 'remote_redundancy.videos.accept_from', - 'federation.enabled', 'federation.videos.federate_unlisted', 'federation.videos.cleanup_remote_interactions', + 'federation.enabled', 'federation.prevent_ssrf', 'federation.videos.federate_unlisted', 'federation.videos.cleanup_remote_interactions', 'peertube.check_latest_version.enabled', 'peertube.check_latest_version.url', 'search.remote_uri.users', 'search.remote_uri.anonymous', 'search.search_index.enabled', 'search.search_index.url', 'search.search_index.disable_local_search', 'search.search_index.is_default_search', diff --git a/server/core/initializers/config.ts b/server/core/initializers/config.ts index 6a6522f5c..65ccb62a9 100644 --- a/server/core/initializers/config.ts +++ b/server/core/initializers/config.ts @@ -342,6 +342,7 @@ const CONFIG = { }, FEDERATION: { ENABLED: config.get('federation.enabled'), + PREVENT_SSRF: config.get('federation.prevent_ssrf'), VIDEOS: { FEDERATE_UNLISTED: config.get('federation.videos.federate_unlisted'), CLEANUP_REMOTE_INTERACTIONS: config.get('federation.videos.cleanup_remote_interactions') diff --git a/server/core/lib/job-queue/handlers/activitypub-follow.ts b/server/core/lib/job-queue/handlers/activitypub-follow.ts index 02c85cfe4..6cf4428e0 100644 --- a/server/core/lib/job-queue/handlers/activitypub-follow.ts +++ b/server/core/lib/job-queue/handlers/activitypub-follow.ts @@ -40,7 +40,7 @@ async function processActivityPubFollow (job: Job) { targetActor = await getOrCreateAPActor(actorUrl, 'all') } catch (err) { - logger.warn(`Do not follow ${handle} because we could not find the actor URL (in database or using webfinger)`) + logger.warn(`Do not follow ${handle} because we could not find the actor URL (in database or using webfinger)`, { err }) return } } diff --git a/server/core/lib/object-storage/shared/client.ts b/server/core/lib/object-storage/shared/client.ts index fbbb2480a..6cd6263e2 100644 --- a/server/core/lib/object-storage/shared/client.ts +++ b/server/core/lib/object-storage/shared/client.ts @@ -1,14 +1,14 @@ import type { S3Client } from '@aws-sdk/client-s3' import { logger } from '@server/helpers/logger.js' import { isProxyEnabled } from '@server/helpers/proxy.js' -import { getAgent } from '@server/helpers/requests.js' +import { getProxyAgent } from '@server/helpers/requests.js' import { CONFIG } from '@server/initializers/config.js' import { lTags } from './logger.js' async function getProxyRequestHandler () { if (!isProxyEnabled()) return null - const { agent } = getAgent() + const { agent } = getProxyAgent() const { NodeHttpHandler } = await import('@smithy/node-http-handler') diff --git a/server/core/lib/plugins/plugin-index.ts b/server/core/lib/plugins/plugin-index.ts index 0f436f6b8..c96f6ff2c 100644 --- a/server/core/lib/plugins/plugin-index.ts +++ b/server/core/lib/plugins/plugin-index.ts @@ -1,9 +1,3 @@ -import { sanitizeUrl } from '@server/helpers/core-utils.js' -import { logger } from '@server/helpers/logger.js' -import { doJSONRequest } from '@server/helpers/requests.js' -import { CONFIG } from '@server/initializers/config.js' -import { PEERTUBE_VERSION } from '@server/initializers/constants.js' -import { PluginModel } from '@server/models/server/plugin.js' import { PeerTubePluginIndex, PeertubePluginIndexList, @@ -11,6 +5,12 @@ import { PeertubePluginLatestVersionResponse, ResultList } from '@peertube/peertube-models' +import { sanitizeUrl } from '@server/helpers/core-utils.js' +import { logger } from '@server/helpers/logger.js' +import { doJSONRequest } from '@server/helpers/requests.js' +import { CONFIG } from '@server/initializers/config.js' +import { PEERTUBE_VERSION } from '@server/initializers/constants.js' +import { PluginModel } from '@server/models/server/plugin.js' import { PluginManager } from './plugin-manager.js' async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList) { @@ -28,7 +28,7 @@ async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList) const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins' try { - const { body } = await doJSONRequest(uri, { searchParams }) + const { body } = await doJSONRequest(uri, { searchParams, preventSSRF: false }) logger.debug('Got result from PeerTube index.', { body }) @@ -57,12 +57,7 @@ async function getLatestPluginsVersion (npmNames: string[]): Promise(uri, options) + const { body } = await doJSONRequest(uri, { json: bodyRequest, method: 'POST', preventSSRF: false }) return body } @@ -79,7 +74,6 @@ async function getLatestPluginVersion (npmName: string) { } export { - listAvailablePluginsFromIndex, - getLatestPluginVersion, - getLatestPluginsVersion + getLatestPluginsVersion, getLatestPluginVersion, listAvailablePluginsFromIndex } + diff --git a/server/core/lib/schedulers/auto-follow-index-instances.ts b/server/core/lib/schedulers/auto-follow-index-instances.ts index 47a01ec0c..86678555d 100644 --- a/server/core/lib/schedulers/auto-follow-index-instances.ts +++ b/server/core/lib/schedulers/auto-follow-index-instances.ts @@ -39,7 +39,7 @@ export class AutoFollowIndexInstances extends AbstractScheduler { this.lastCheck = new Date() - const { body } = await doJSONRequest(indexUrl, { searchParams }) + const { body } = await doJSONRequest(indexUrl, { searchParams, preventSSRF: false }) if (!body.data || Array.isArray(body.data) === false) { logger.error('Cannot auto follow instances of index %s. Please check the auto follow URL.', indexUrl, { body }) return diff --git a/server/core/lib/schedulers/peertube-version-check-scheduler.ts b/server/core/lib/schedulers/peertube-version-check-scheduler.ts index d9d9c7cb0..347bdbb38 100644 --- a/server/core/lib/schedulers/peertube-version-check-scheduler.ts +++ b/server/core/lib/schedulers/peertube-version-check-scheduler.ts @@ -27,7 +27,7 @@ export class PeerTubeVersionCheckScheduler extends AbstractScheduler { logger.info('Checking latest PeerTube version.') - const { body } = await doJSONRequest(CONFIG.PEERTUBE.CHECK_LATEST_VERSION.URL) + const { body } = await doJSONRequest(CONFIG.PEERTUBE.CHECK_LATEST_VERSION.URL, { preventSSRF: false }) if (!body?.peertube?.latestVersion) { logger.warn('Cannot check latest PeerTube version: body is invalid.', { body }) diff --git a/yarn.lock b/yarn.lock index b810f9a2e..d22e57ea7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6151,6 +6151,14 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" +got-ssrf@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/got-ssrf/-/got-ssrf-3.0.0.tgz#400962f6157ed7ca86024adc274f7c49814d6f3f" + integrity sha512-Cg9lmGkaNzT8cUSDbnb1GIgetnsrwCSA8S6ZW6NiY37cEbJ/HoDs/LIradAzLcrL62XPcK6eP1442OFvrNzoIg== + dependencies: + debug "^4.3.2" + ipaddr.js "^2.0.1" + got@^13.0.0: version "13.0.0" resolved "https://registry.yarnpkg.com/got/-/got-13.0.0.tgz#a2402862cef27a5d0d1b07c0fb25d12b58175422" @@ -6599,7 +6607,7 @@ ipaddr.js@1.9.1: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== -ipaddr.js@2.2.0, "ipaddr.js@>= 0.1.5", ipaddr.js@^2.0.0: +ipaddr.js@2.2.0, "ipaddr.js@>= 0.1.5", ipaddr.js@^2.0.0, ipaddr.js@^2.0.1: version "2.2.0" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==