From 922c3acab3785e4fd81dcf92942f7382d975df70 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Wed, 18 Jan 2023 15:09:49 -0800 Subject: [PATCH] Fix share URL permissions. (#1745) * Fix share URL permissions. * Add sql param logic. * Add permissions to edit website. * Update permissions. * Move parameters to param injection. * Sanitize eventdata. * Remove caret. * Fix avg. --- components/layout/Header.js | 29 +++++---- lib/auth.js | 4 +- lib/prisma.js | 60 ++++++++++++++----- pages/api/accounts/[id]/password.js | 2 +- pages/api/realtime/init.js | 6 +- pages/api/websites/[id]/index.js | 14 +++-- pages/api/websites/[id]/reset.js | 2 +- pages/api/websites/index.js | 3 +- queries/analytics/event/getEventData.js | 41 +++++++++---- queries/analytics/event/getEventMetrics.js | 8 +-- .../analytics/pageview/getPageviewMetrics.js | 8 +-- .../analytics/pageview/getPageviewParams.js | 8 +-- .../analytics/pageview/getPageviewStats.js | 8 +-- .../analytics/session/getSessionMetrics.js | 8 +-- queries/analytics/stats/getActiveVisitors.js | 9 +-- queries/analytics/stats/getWebsiteStats.js | 8 +-- 16 files changed, 139 insertions(+), 79 deletions(-) diff --git a/components/layout/Header.js b/components/layout/Header.js index a81e016c..5f0ca386 100644 --- a/components/layout/Header.js +++ b/components/layout/Header.js @@ -29,21 +29,24 @@ export default function Header() { } size="large" className={styles.logo} /> umami - + {user && ( -
- - - - - - - {!process.env.isCloudMode && ( - - + <> + +
+ + - )} -
+ + + + {!process.env.isCloudMode && ( + + + + )} +
+ )}
diff --git a/lib/auth.js b/lib/auth.js index 93027544..d485b977 100644 --- a/lib/auth.js +++ b/lib/auth.js @@ -35,7 +35,7 @@ export function isValidToken(token, validation) { return false; } -export async function allowQuery(req, type) { +export async function allowQuery(req, type, allowShareToken = true) { const { id } = req.query; const { userId, isAdmin, shareToken } = req.auth ?? {}; @@ -44,7 +44,7 @@ export async function allowQuery(req, type) { return true; } - if (shareToken) { + if (allowShareToken && shareToken) { return isValidToken(shareToken, { id }); } diff --git a/lib/prisma.js b/lib/prisma.js index ab1e6ebf..109e1ae3 100644 --- a/lib/prisma.js +++ b/lib/prisma.js @@ -36,6 +36,18 @@ function logQuery(e) { log(chalk.yellow(e.params), '->', e.query, chalk.greenBright(`${e.duration}ms`)); } +function toUuid() { + const db = getDatabaseType(process.env.DATABASE_URL); + + if (db === POSTGRESQL) { + return '::uuid'; + } + + if (db === MYSQL) { + return ''; + } +} + function getClient(options) { const prisma = new PrismaClient(options); @@ -85,11 +97,23 @@ function getTimestampInterval(field) { } } -function getJsonField(column, property, isNumber) { +function getSanitizedColumns(columns) { + return Object.keys(columns).reduce((acc, keyName) => { + const sanitizedProperty = keyName.replace(/[\w\s_]/g, ''); + + acc[sanitizedProperty] = columns[keyName]; + + return acc; + }, {}); +} + +function getJsonField(column, property, isNumber, params) { const db = getDatabaseType(process.env.DATABASE_URL); if (db === POSTGRESQL) { - let accessor = `${column} ->> '${property}'`; + params.push(property); + + let accessor = `${column} ->> $${params.length}`; if (isNumber) { accessor = `CAST(${accessor} AS DECIMAL)`; @@ -99,21 +123,29 @@ function getJsonField(column, property, isNumber) { } if (db === MYSQL) { - return `${column} ->> "$.${property}"`; + return `${column} ->> '$.${property}'`; } } -function getEventDataColumnsQuery(column, columns) { - const query = Object.keys(columns).reduce((arr, key) => { +function getEventDataColumnsQuery(column, columns, params) { + const query = Object.keys(columns).reduce((arr, key, i) => { const filter = columns[key]; if (filter === undefined) { return arr; } - const isNumber = ['sum', 'avg', 'min', 'max'].some(a => a === filter); - - arr.push(`${filter}(${getJsonField(column, key, isNumber)}) as "${filter}(${key})"`); + switch (filter) { + case 'sum': + case 'avg': + case 'min': + case 'max': + arr.push(`${filter}(${getJsonField(column, key, true, params)}) as "${i}"`); + break; + case 'count': + arr.push(`${filter}(${getJsonField(column, key, false, params)}) as "${i}"`); + break; + } return arr; }, []); @@ -121,7 +153,7 @@ function getEventDataColumnsQuery(column, columns) { return query.join(',\n'); } -function getEventDataFilterQuery(column, filters) { +function getEventDataFilterQuery(column, filters, params) { const query = Object.keys(filters).reduce((arr, key) => { const filter = filters[key]; @@ -131,11 +163,9 @@ function getEventDataFilterQuery(column, filters) { const isNumber = filter && typeof filter === 'number'; - arr.push( - `${getJsonField(column, key, isNumber)} = ${ - typeof filter === 'string' ? `'${filter}'` : filter - }`, - ); + arr.push(`${getJsonField(column, key, isNumber, params)} = $${params.length + 1}`); + + params.push(filter); return arr; }, []); @@ -248,11 +278,13 @@ const prisma = global[PRISMA] || getClient(PRISMA_OPTIONS); export default { client: prisma, log, + toUuid, getDateQuery, getTimestampInterval, getFilterQuery, getEventDataColumnsQuery, getEventDataFilterQuery, + getSanitizedColumns, parseFilters, rawQuery, transaction, diff --git a/pages/api/accounts/[id]/password.js b/pages/api/accounts/[id]/password.js index 89649c20..5dde1258 100644 --- a/pages/api/accounts/[id]/password.js +++ b/pages/api/accounts/[id]/password.js @@ -17,7 +17,7 @@ export default async (req, res) => { const { current_password, new_password } = req.body; const { id: accountUuid } = req.query; - if (!(await allowQuery(req, TYPE_ACCOUNT))) { + if (!(await allowQuery(req, TYPE_ACCOUNT, false))) { return unauthorized(res); } diff --git a/pages/api/realtime/init.js b/pages/api/realtime/init.js index 9a9a4297..75ddf022 100644 --- a/pages/api/realtime/init.js +++ b/pages/api/realtime/init.js @@ -1,5 +1,5 @@ import { subMinutes } from 'date-fns'; -import { ok, methodNotAllowed, createToken } from 'next-basics'; +import { ok, unauthorized, methodNotAllowed, createToken } from 'next-basics'; import { useAuth } from 'lib/middleware'; import { getUserWebsites, getRealtimeData } from 'queries'; import { secret } from 'lib/crypto'; @@ -10,6 +10,10 @@ export default async (req, res) => { if (req.method === 'GET') { const { userId } = req.auth; + if (!userId) { + return unauthorized(res); + } + const websites = await getUserWebsites({ userId }); const ids = websites.map(({ websiteUuid }) => websiteUuid); const token = createToken({ websites: ids }, secret()); diff --git a/pages/api/websites/[id]/index.js b/pages/api/websites/[id]/index.js index a3771427..74f11c95 100644 --- a/pages/api/websites/[id]/index.js +++ b/pages/api/websites/[id]/index.js @@ -10,17 +10,21 @@ export default async (req, res) => { const { id: websiteUuid } = req.query; - if (!(await allowQuery(req, TYPE_WEBSITE))) { - return unauthorized(res); - } - if (req.method === 'GET') { + if (!(await allowQuery(req, TYPE_WEBSITE))) { + return unauthorized(res); + } + const website = await getWebsite({ websiteUuid }); return ok(res, website); } if (req.method === 'POST') { + if (!(await allowQuery(req, TYPE_WEBSITE, false))) { + return unauthorized(res); + } + const { name, domain, owner, enableShareUrl, shareId } = req.body; const { accountUuid } = req.auth; @@ -58,7 +62,7 @@ export default async (req, res) => { } if (req.method === 'DELETE') { - if (!(await allowQuery(req, TYPE_WEBSITE))) { + if (!(await allowQuery(req, TYPE_WEBSITE, false))) { return unauthorized(res); } diff --git a/pages/api/websites/[id]/reset.js b/pages/api/websites/[id]/reset.js index 0dde02df..0075a74d 100644 --- a/pages/api/websites/[id]/reset.js +++ b/pages/api/websites/[id]/reset.js @@ -11,7 +11,7 @@ export default async (req, res) => { const { id: websiteId } = req.query; if (req.method === 'POST') { - if (!(await allowQuery(req, TYPE_WEBSITE))) { + if (!(await allowQuery(req, TYPE_WEBSITE, false))) { return unauthorized(res); } diff --git a/pages/api/websites/index.js b/pages/api/websites/index.js index daecac88..f5fb6cfe 100644 --- a/pages/api/websites/index.js +++ b/pages/api/websites/index.js @@ -7,6 +7,7 @@ export default async (req, res) => { await useAuth(req, res); const { user_id, include_all } = req.query; + const { userId: currentUserId, isAdmin } = req.auth; const accountUuid = user_id || req.auth.accountUuid; let account; @@ -18,7 +19,7 @@ export default async (req, res) => { const userId = account ? account.id : user_id; if (req.method === 'GET') { - if (userId && userId !== currentUserId && !isAdmin) { + if (!userId || (userId !== currentUserId && !isAdmin)) { return unauthorized(res); } diff --git a/queries/analytics/event/getEventData.js b/queries/analytics/event/getEventData.js index 91302d30..08d7bc03 100644 --- a/queries/analytics/event/getEventData.js +++ b/queries/analytics/event/getEventData.js @@ -10,29 +10,44 @@ export async function getEventData(...args) { } async function relationalQuery(websiteId, { startDate, endDate, event_name, columns, filters }) { - const { rawQuery, getEventDataColumnsQuery, getEventDataFilterQuery } = prisma; - const params = [startDate, endDate]; + const { + rawQuery, + getEventDataColumnsQuery, + getEventDataFilterQuery, + toUuid, + getSanitizedColumns, + } = prisma; + const sanitizedColumns = getSanitizedColumns(columns); + const params = [websiteId, startDate, endDate]; + + if (event_name) { + params.push(event_name); + } + + const columnQuery = getEventDataColumnsQuery('event_data.event_data', sanitizedColumns, params); + const filterQuery = + Object.keys(filters).length > 0 + ? `and ${getEventDataFilterQuery('event_data.event_data', filters, params)}` + : ''; return rawQuery( `select - ${getEventDataColumnsQuery('event_data.event_data', columns)} + ${columnQuery} from event join website on event.website_id = website.website_id join event_data on event.event_id = event_data.event_id - where website_uuid='${websiteId}' - and event.created_at between $1 and $2 - ${event_name ? `and event_name = ${event_name}` : ''} - ${ - Object.keys(filters).length > 0 - ? `and ${getEventDataFilterQuery('event_data.event_data', filters)}` - : '' - }`, + where website_uuid = $1${toUuid()} + and event.created_at between $2 and $3 + ${event_name ? `and event_name = $4` : ''} + ${filterQuery}`, params, ).then(results => { - return Object.keys(results[0]).map(a => { - return { x: a, y: results[0][`${a}`] }; + const fields = Object.keys(sanitizedColumns); + + return Object.keys(results[0]).map((a, i) => { + return { x: `${sanitizedColumns[fields[i]]}(${fields[i]})`, y: results[0][i] }; }); }); } diff --git a/queries/analytics/event/getEventMetrics.js b/queries/analytics/event/getEventMetrics.js index 605bb688..d40703d7 100644 --- a/queries/analytics/event/getEventMetrics.js +++ b/queries/analytics/event/getEventMetrics.js @@ -17,8 +17,8 @@ async function relationalQuery( unit = 'day', filters = {}, ) { - const { rawQuery, getDateQuery, getFilterQuery } = prisma; - const params = [start_at, end_at]; + const { rawQuery, getDateQuery, getFilterQuery, toUuid } = prisma; + const params = [websiteId, start_at, end_at]; return rawQuery( `select @@ -28,8 +28,8 @@ async function relationalQuery( from event join website on event.website_id = website.website_id - where website_uuid='${websiteId}' - and event.created_at between $1 and $2 + where website_uuid = $1${toUuid()} + and event.created_at between $2 and $3 ${getFilterQuery('event', filters, params)} group by 1, 2 order by 2`, diff --git a/queries/analytics/pageview/getPageviewMetrics.js b/queries/analytics/pageview/getPageviewMetrics.js index 69607d00..58c1e1b0 100644 --- a/queries/analytics/pageview/getPageviewMetrics.js +++ b/queries/analytics/pageview/getPageviewMetrics.js @@ -10,8 +10,8 @@ export async function getPageviewMetrics(...args) { } async function relationalQuery(websiteId, { startDate, endDate, column, table, filters = {} }) { - const { rawQuery, parseFilters } = prisma; - const params = [startDate, endDate]; + const { rawQuery, parseFilters, toUuid } = prisma; + const params = [websiteId, startDate, endDate]; const { pageviewQuery, sessionQuery, eventQuery, joinSession } = parseFilters( table, column, @@ -24,8 +24,8 @@ async function relationalQuery(websiteId, { startDate, endDate, column, table, f from ${table} ${` join website on ${table}.website_id = website.website_id`} ${joinSession} - where website.website_uuid='${websiteId}' - and ${table}.created_at between $1 and $2 + where website.website_uuid = $1${toUuid()} + and ${table}.created_at between $2 and $3 ${pageviewQuery} ${joinSession && sessionQuery} ${eventQuery} diff --git a/queries/analytics/pageview/getPageviewParams.js b/queries/analytics/pageview/getPageviewParams.js index 5cdabfa3..2ccabe23 100644 --- a/queries/analytics/pageview/getPageviewParams.js +++ b/queries/analytics/pageview/getPageviewParams.js @@ -9,8 +9,8 @@ export async function getPageviewParams(...args) { } async function relationalQuery(websiteId, start_at, end_at, column, table, filters = {}) { - const { parseFilters, rawQuery } = prisma; - const params = [start_at, end_at]; + const { parseFilters, rawQuery, toUuid } = prisma; + const params = [websiteId, start_at, end_at]; const { pageviewQuery, sessionQuery, eventQuery, joinSession } = parseFilters( table, column, @@ -24,8 +24,8 @@ async function relationalQuery(websiteId, start_at, end_at, column, table, filte from ${table} ${` join website on ${table}.website_id = website.website_id`} ${joinSession} - where website.website_uuid='${websiteId}' - and ${table}.created_at between $1 and $2 + where website.website_uuid = $1${toUuid()} + and ${table}.created_at between $2 and $3 and ${table}.url like '%?%' ${pageviewQuery} ${joinSession && sessionQuery} diff --git a/queries/analytics/pageview/getPageviewStats.js b/queries/analytics/pageview/getPageviewStats.js index 5ec8339f..1f31d31b 100644 --- a/queries/analytics/pageview/getPageviewStats.js +++ b/queries/analytics/pageview/getPageviewStats.js @@ -21,8 +21,8 @@ async function relationalQuery( sessionKey = 'session_id', }, ) { - const { getDateQuery, parseFilters, rawQuery } = prisma; - const params = [start_at, end_at]; + const { getDateQuery, parseFilters, rawQuery, toUuid } = prisma; + const params = [websiteId, start_at, end_at]; const { pageviewQuery, sessionQuery, joinSession } = parseFilters( 'pageview', null, @@ -37,8 +37,8 @@ async function relationalQuery( join website on pageview.website_id = website.website_id ${joinSession} - where website.website_uuid='${websiteId}' - and pageview.created_at between $1 and $2 + where website.website_uuid = $1${toUuid()} + and pageview.created_at between $2 and $3 ${pageviewQuery} ${sessionQuery} group by 1`, diff --git a/queries/analytics/session/getSessionMetrics.js b/queries/analytics/session/getSessionMetrics.js index 020bddfb..bf9c8079 100644 --- a/queries/analytics/session/getSessionMetrics.js +++ b/queries/analytics/session/getSessionMetrics.js @@ -10,8 +10,8 @@ export async function getSessionMetrics(...args) { } async function relationalQuery(websiteId, { startDate, endDate, field, filters = {} }) { - const { parseFilters, rawQuery } = prisma; - const params = [startDate, endDate]; + const { parseFilters, rawQuery, toUuid } = prisma; + const params = [websiteId, startDate, endDate]; const { pageviewQuery, sessionQuery, joinSession } = parseFilters(null, filters, params); return rawQuery( @@ -23,8 +23,8 @@ async function relationalQuery(websiteId, { startDate, endDate, field, filters = join website on pageview.website_id = website.website_id ${joinSession} - where website.website_uuid='${websiteId}' - and pageview.created_at between $1 and $2 + where website.website_uuid = $1${toUuid()} + and pageview.created_at between $2 and $3 ${pageviewQuery} ${sessionQuery} ) diff --git a/queries/analytics/stats/getActiveVisitors.js b/queries/analytics/stats/getActiveVisitors.js index 3a898d94..3cb525d2 100644 --- a/queries/analytics/stats/getActiveVisitors.js +++ b/queries/analytics/stats/getActiveVisitors.js @@ -11,16 +11,17 @@ export async function getActiveVisitors(...args) { } async function relationalQuery(websiteId) { + const { rawQuery, toUuid } = prisma; const date = subMinutes(new Date(), 5); - const params = [date]; + const params = [websiteId, date]; - return prisma.rawQuery( + return rawQuery( `select count(distinct session_id) x from pageview join website on pageview.website_id = website.website_id - where website.website_uuid = '${websiteId}' - and pageview.created_at >= $1`, + where website.website_uuid = $1${toUuid()} + and pageview.created_at >= $2`, params, ); } diff --git a/queries/analytics/stats/getWebsiteStats.js b/queries/analytics/stats/getWebsiteStats.js index 134e1c3e..9a2452a9 100644 --- a/queries/analytics/stats/getWebsiteStats.js +++ b/queries/analytics/stats/getWebsiteStats.js @@ -10,8 +10,8 @@ export async function getWebsiteStats(...args) { } async function relationalQuery(websiteId, { start_at, end_at, filters = {} }) { - const { getDateQuery, getTimestampInterval, parseFilters, rawQuery } = prisma; - const params = [start_at, end_at]; + const { getDateQuery, getTimestampInterval, parseFilters, rawQuery, toUuid } = prisma; + const params = [websiteId, start_at, end_at]; const { pageviewQuery, sessionQuery, joinSession } = parseFilters( 'pageview', null, @@ -33,8 +33,8 @@ async function relationalQuery(websiteId, { start_at, end_at, filters = {} }) { join website on pageview.website_id = website.website_id ${joinSession} - where website.website_uuid='${websiteId}' - and pageview.created_at between $1 and $2 + where website.website_uuid = $1${toUuid()} + and pageview.created_at between $2 and $3 ${pageviewQuery} ${sessionQuery} group by 1, 2