diff --git a/apps/api/package.json b/apps/api/package.json index 6bf07684..2ed53e2c 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -24,6 +24,7 @@ "pino-pretty": "^10.3.1", "ramda": "^0.29.1", "sharp": "^0.33.2", + "sqlstring": "^2.3.3", "ua-parser-js": "^1.0.37", "url-metadata": "^4.1.0", "uuid": "^9.0.1" @@ -34,6 +35,7 @@ "@openpanel/sdk": "workspace:*", "@openpanel/tsconfig": "workspace:*", "@types/ramda": "^0.29.6", + "@types/sqlstring": "^2.3.2", "@types/ua-parser-js": "^0.7.39", "@types/uuid": "^9.0.8", "@types/ws": "^8.5.10", diff --git a/apps/api/src/controllers/event.controller.ts b/apps/api/src/controllers/event.controller.ts index afefb11e..da814b17 100644 --- a/apps/api/src/controllers/event.controller.ts +++ b/apps/api/src/controllers/event.controller.ts @@ -5,6 +5,7 @@ import { isUserAgentSet, parseUserAgent } from '@/utils/parseUserAgent'; import { isSameDomain, parsePath } from '@/utils/url'; import type { FastifyReply, FastifyRequest } from 'fastify'; import { omit } from 'ramda'; +import { escape } from 'sqlstring'; import { v4 as uuid } from 'uuid'; import { generateDeviceId, getTime, toISOString } from '@openpanel/common'; @@ -103,7 +104,7 @@ export async function postEvent( const [event] = await withTiming( 'Get last event (server-event)', getEvents( - `SELECT * FROM events WHERE name = 'screen_view' AND profile_id = '${profileId}' AND project_id = '${projectId}' ORDER BY created_at DESC LIMIT 1` + `SELECT * FROM events WHERE name = 'screen_view' AND profile_id = ${escape(profileId)} AND project_id = ${escape(projectId)} ORDER BY created_at DESC LIMIT 1` ) ); @@ -212,7 +213,7 @@ export async function postEvent( 'Get session start event', Promise.all([ getEvents( - `SELECT * FROM events WHERE name = 'session_start' AND device_id = '${deviceId}' AND project_id = '${projectId}' ORDER BY created_at DESC LIMIT 1` + `SELECT * FROM events WHERE name = 'session_start' AND device_id = ${escape(deviceId)} AND project_id = ${escape(projectId)} ORDER BY created_at DESC LIMIT 1` ), findJobByPrefix(eventsQueue, `event:${projectId}:${deviceId}:`), ]) diff --git a/apps/api/src/controllers/live.controller.ts b/apps/api/src/controllers/live.controller.ts index 93de1e83..a47565c5 100644 --- a/apps/api/src/controllers/live.controller.ts +++ b/apps/api/src/controllers/live.controller.ts @@ -1,4 +1,5 @@ import type { FastifyReply, FastifyRequest } from 'fastify'; +import { escape } from 'sqlstring'; import type * as WebSocket from 'ws'; import { getSafeJson } from '@openpanel/common'; @@ -19,7 +20,7 @@ export async function test( reply: FastifyReply ) { const [event] = await getEvents( - `SELECT * FROM events WHERE project_id = '${req.params.projectId}' AND name = 'screen_view' LIMIT 1` + `SELECT * FROM events WHERE project_id = ${escape(req.params.projectId)} AND name = 'screen_view' LIMIT 1` ); if (!event) { return reply.status(404).send('No event found'); diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 5db03c8f..8ea4baea 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -90,6 +90,7 @@ "short-unique-id": "^5.0.3", "slugify": "^1.6.6", "sonner": "^1.4.0", + "sqlstring": "^2.3.3", "superjson": "^1.13.3", "tailwind-merge": "^1.14.0", "tailwindcss-animate": "^1.0.7", @@ -109,6 +110,7 @@ "@types/react-dom": "^18.2.7", "@types/react-syntax-highlighter": "^15.5.11", "@types/request-ip": "^0.0.41", + "@types/sqlstring": "^2.3.2", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "autoprefixer": "^10.4.17", diff --git a/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/events/event-conversions-list/index.tsx b/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/events/event-conversions-list/index.tsx index ba50a6f3..d5b5a97b 100644 --- a/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/events/event-conversions-list/index.tsx +++ b/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/events/event-conversions-list/index.tsx @@ -1,4 +1,5 @@ import { Widget } from '@/components/widget'; +import { escape } from 'sqlstring'; import { db, getEvents } from '@openpanel/db'; @@ -21,7 +22,7 @@ export default async function EventConversionsListServer({ projectId }: Props) { } const events = await getEvents( - `SELECT * FROM events WHERE project_id = '${projectId}' AND name IN (${conversions.map((c) => `'${c.name}'`).join(', ')}) ORDER BY created_at DESC LIMIT 20;`, + `SELECT * FROM events WHERE project_id = ${escape(projectId)} AND name IN (${conversions.map((c) => escape(c.name)).join(', ')}) ORDER BY created_at DESC LIMIT 20;`, { profile: true, meta: true, diff --git a/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/profiles/profile-last-seen/index.tsx b/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/profiles/profile-last-seen/index.tsx index 59a10651..0c0cb4ce 100644 --- a/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/profiles/profile-last-seen/index.tsx +++ b/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/profiles/profile-last-seen/index.tsx @@ -5,6 +5,7 @@ import { } from '@/components/ui/tooltip'; import { Widget, WidgetBody, WidgetHead } from '@/components/widget'; import { cn } from '@/utils/cn'; +import { escape } from 'sqlstring'; import { chQuery } from '@openpanel/db'; @@ -20,7 +21,7 @@ export default async function ProfileLastSeenServer({ projectId }: Props) { // Days since last event from users // group by days const res = await chQuery( - `SELECT age('days',created_at, now()) as days, count(distinct profile_id) as count FROM events where project_id = '${projectId}' group by days order by days ASC` + `SELECT age('days',created_at, now()) as days, count(distinct profile_id) as count FROM events where project_id = ${escape(projectId)} group by days order by days ASC` ); const take = 18; diff --git a/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/profiles/profile-top/index.tsx b/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/profiles/profile-top/index.tsx index 2abb8739..d51661a3 100644 --- a/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/profiles/profile-top/index.tsx +++ b/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/profiles/profile-top/index.tsx @@ -4,6 +4,7 @@ import { Widget, WidgetHead } from '@/components/widget'; import { WidgetTable } from '@/components/widget-table'; import { getProfileName } from '@/utils/getters'; import Link from 'next/link'; +import { escape } from 'sqlstring'; import { chQuery, getProfiles } from '@openpanel/db'; @@ -19,7 +20,7 @@ export default async function ProfileTopServer({ // Days since last event from users // group by days const res = await chQuery<{ profile_id: string; count: number }>( - `SELECT profile_id, count(*) as count from events where profile_id != '' and project_id = '${projectId}' group by profile_id order by count() DESC LIMIT 10` + `SELECT profile_id, count(*) as count from events where profile_id != '' and project_id = ${escape(projectId)} group by profile_id order by count() DESC LIMIT 10` ); const profiles = await getProfiles({ ids: res.map((r) => r.profile_id) }); const list = res.map((item) => { diff --git a/apps/dashboard/src/components/projects/project-card.tsx b/apps/dashboard/src/components/projects/project-card.tsx index 61d8630d..35e80a53 100644 --- a/apps/dashboard/src/components/projects/project-card.tsx +++ b/apps/dashboard/src/components/projects/project-card.tsx @@ -1,5 +1,6 @@ import { shortNumber } from '@/hooks/useNumerFormatter'; import Link from 'next/link'; +import { escape } from 'sqlstring'; import type { IServiceProject } from '@openpanel/db'; import { chQuery } from '@openpanel/db'; @@ -13,19 +14,19 @@ export async function ProjectCard({ }: IServiceProject) { const [chart, [data]] = await Promise.all([ chQuery<{ value: number; date: string }>( - `SELECT countDistinct(profile_id) as value, toStartOfDay(created_at) as date FROM events WHERE project_id = '${id}' AND name = 'session_start' AND created_at >= now() - interval '1 month' GROUP BY date ORDER BY date ASC` + `SELECT countDistinct(profile_id) as value, toStartOfDay(created_at) as date FROM events WHERE project_id = ${escape(id)} AND name = 'session_start' AND created_at >= now() - interval '1 month' GROUP BY date ORDER BY date ASC` ), chQuery<{ total: number; month: number; day: number }>( ` SELECT ( - SELECT count(DISTINCT profile_id) as count FROM events WHERE project_id = '${id}' + SELECT count(DISTINCT profile_id) as count FROM events WHERE project_id = ${escape(id)} ) as total, ( - SELECT count(DISTINCT profile_id) as count FROM events WHERE project_id = '${id}' AND created_at >= now() - interval '1 month' + SELECT count(DISTINCT profile_id) as count FROM events WHERE project_id = ${escape(id)} AND created_at >= now() - interval '1 month' ) as month, ( - SELECT count(DISTINCT profile_id) as count FROM events WHERE project_id = '${id}' AND created_at >= now() - interval '1 day' + SELECT count(DISTINCT profile_id) as count FROM events WHERE project_id = ${escape(id)} AND created_at >= now() - interval '1 day' ) as day ` ), diff --git a/apps/dashboard/src/trpc/api/routers/chart.helpers.ts b/apps/dashboard/src/trpc/api/routers/chart.helpers.ts index 204f1e42..867d4b0d 100644 --- a/apps/dashboard/src/trpc/api/routers/chart.helpers.ts +++ b/apps/dashboard/src/trpc/api/routers/chart.helpers.ts @@ -2,6 +2,7 @@ import { round } from '@/utils/math'; import { subDays } from 'date-fns'; import * as mathjs from 'mathjs'; import { repeat, reverse, sort } from 'ramda'; +import { escape } from 'sqlstring'; import { alphabetIds, NOT_SET_VALUE } from '@openpanel/constants'; import { @@ -430,7 +431,7 @@ export async function getFunnelData({ projectId, ...payload }: IChartInput) { const funnels = payload.events.map((event) => { const { sb, getWhere } = createSqlBuilder(); sb.where = getEventFiltersWhereClause(event.filters); - sb.where.name = `name = '${event.name}'`; + sb.where.name = `name = ${escape(event.name)}`; return getWhere().replace('WHERE ', ''); }); @@ -438,7 +439,7 @@ export async function getFunnelData({ projectId, ...payload }: IChartInput) { session_id, windowFunnel(6048000000000000,'strict_increase')(toUnixTimestamp(created_at), ${funnels.join(', ')}) AS level FROM events - WHERE (project_id = '${projectId}' AND created_at >= '${formatClickhouseDate(startDate)}') AND (created_at <= '${formatClickhouseDate(endDate)}') + WHERE (project_id = ${escape(projectId)} AND created_at >= '${formatClickhouseDate(startDate)}') AND (created_at <= '${formatClickhouseDate(endDate)}') GROUP BY session_id`; const sql = `SELECT level, count() AS count FROM (${innerSql}) GROUP BY level ORDER BY level DESC`; @@ -446,7 +447,7 @@ export async function getFunnelData({ projectId, ...payload }: IChartInput) { const [funnelRes, sessionRes] = await Promise.all([ chQuery<{ level: number; count: number }>(sql), chQuery<{ count: number }>( - `SELECT count(name) as count FROM events WHERE project_id = '${projectId}' AND name = 'session_start' AND (created_at >= '${formatClickhouseDate(startDate)}') AND (created_at <= '${formatClickhouseDate(endDate)}')` + `SELECT count(name) as count FROM events WHERE project_id = ${escape(projectId)} AND name = 'session_start' AND (created_at >= '${formatClickhouseDate(startDate)}') AND (created_at <= '${formatClickhouseDate(endDate)}')` ), ]); diff --git a/apps/dashboard/src/trpc/api/routers/chart.ts b/apps/dashboard/src/trpc/api/routers/chart.ts index 5177b1a8..d5756083 100644 --- a/apps/dashboard/src/trpc/api/routers/chart.ts +++ b/apps/dashboard/src/trpc/api/routers/chart.ts @@ -5,6 +5,7 @@ import { } from '@/trpc/api/trpc'; import { average, max, min, round, sum } from '@/utils/math'; import { flatten, map, pipe, prop, sort, uniq } from 'ramda'; +import { escape } from 'sqlstring'; import { z } from 'zod'; import { chQuery, createSqlBuilder } from '@openpanel/db'; @@ -60,7 +61,7 @@ export const chartRouter = createTRPCRouter({ .input(z.object({ projectId: z.string() })) .query(async ({ input: { projectId } }) => { const events = await chQuery<{ name: string }>( - `SELECT DISTINCT name FROM events WHERE project_id = '${projectId}'` + `SELECT DISTINCT name FROM events WHERE project_id = ${escape(projectId)}` ); return [ @@ -76,8 +77,8 @@ export const chartRouter = createTRPCRouter({ .query(async ({ input: { projectId, event } }) => { const events = await chQuery<{ keys: string[] }>( `SELECT distinct mapKeys(properties) as keys from events where ${ - event && event !== '*' ? `name = '${event}' AND ` : '' - } project_id = '${projectId}';` + event && event !== '*' ? `name = ${escape(event)} AND ` : '' + } project_id = ${escape(projectId)};` ); const properties = events @@ -122,14 +123,14 @@ export const chartRouter = createTRPCRouter({ ) .query(async ({ input: { event, property, projectId } }) => { const { sb, getSql } = createSqlBuilder(); - sb.where.project_id = `project_id = '${projectId}'`; + sb.where.project_id = `project_id = ${escape(projectId)}`; if (event !== '*') { - sb.where.event = `name = '${event}'`; + sb.where.event = `name = ${escape(event)}`; } if (property.startsWith('properties.')) { - sb.select.values = `distinct mapValues(mapExtractKeyLike(properties, '${property - .replace(/^properties\./, '') - .replace('.*.', '.%.')}')) as values`; + sb.select.values = `distinct mapValues(mapExtractKeyLike(properties, ${escape( + property.replace(/^properties\./, '').replace('.*.', '.%.') + )})) as values`; } else { sb.select.values = `distinct ${property} as values`; } diff --git a/apps/dashboard/src/trpc/api/routers/profile.ts b/apps/dashboard/src/trpc/api/routers/profile.ts index f1212d74..b1982e83 100644 --- a/apps/dashboard/src/trpc/api/routers/profile.ts +++ b/apps/dashboard/src/trpc/api/routers/profile.ts @@ -4,6 +4,7 @@ import { publicProcedure, } from '@/trpc/api/trpc'; import { flatten, map, pipe, prop, sort, uniq } from 'ramda'; +import { escape } from 'sqlstring'; import { z } from 'zod'; import { chQuery, createSqlBuilder } from '@openpanel/db'; @@ -13,7 +14,7 @@ export const profileRouter = createTRPCRouter({ .input(z.object({ projectId: z.string() })) .query(async ({ input: { projectId } }) => { const events = await chQuery<{ keys: string[] }>( - `SELECT distinct mapKeys(properties) as keys from profiles where project_id = '${projectId}';` + `SELECT distinct mapKeys(properties) as keys from profiles where project_id = ${escape(projectId)};` ); const properties = events @@ -40,11 +41,11 @@ export const profileRouter = createTRPCRouter({ .query(async ({ input: { property, projectId } }) => { const { sb, getSql } = createSqlBuilder(); sb.from = 'profiles'; - sb.where.project_id = `project_id = '${projectId}'`; + sb.where.project_id = `project_id = ${escape(projectId)}`; if (property.startsWith('properties.')) { - sb.select.values = `distinct mapValues(mapExtractKeyLike(properties, '${property - .replace(/^properties\./, '') - .replace('.*.', '.%.')}')) as values`; + sb.select.values = `distinct mapValues(mapExtractKeyLike(properties, ${escape( + property.replace(/^properties\./, '').replace('.*.', '.%.') + )})) as values`; } else { sb.select.values = `${property} as values`; } diff --git a/packages/db/package.json b/packages/db/package.json index 7ea06e8f..9f67313a 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -20,6 +20,7 @@ "@openpanel/validation": "workspace:*", "@prisma/client": "^5.1.1", "ramda": "^0.29.1", + "sqlstring": "^2.3.3", "uuid": "^9.0.1" }, "devDependencies": { @@ -28,6 +29,7 @@ "@openpanel/tsconfig": "workspace:*", "@types/node": "^18.16.0", "@types/ramda": "^0.29.6", + "@types/sqlstring": "^2.3.2", "@types/uuid": "^9.0.8", "eslint": "^8.48.0", "prettier": "^3.0.3", diff --git a/packages/db/src/services/chart.service.ts b/packages/db/src/services/chart.service.ts index bfb74501..6ad179e4 100644 --- a/packages/db/src/services/chart.service.ts +++ b/packages/db/src/services/chart.service.ts @@ -1,10 +1,11 @@ +import { escape } from 'sqlstring'; + import type { IChartEventFilter, IGetChartDataInput, } from '@openpanel/validation'; import { formatClickhouseDate } from '../clickhouse-client'; -import type { SqlBuilderObject } from '../sql-builder'; import { createSqlBuilder } from '../sql-builder'; function log(sql: string) { @@ -25,10 +26,10 @@ export function getChartSql({ createSqlBuilder(); sb.where = getEventFiltersWhereClause(event.filters); - sb.where.projectId = `project_id = '${projectId}'`; + sb.where.projectId = `project_id = ${escape(projectId)}`; if (event.name !== '*') { - sb.select.label = `'${event.name}' as label`; - sb.where.eventName = `name = '${event.name}'`; + sb.select.label = `${escape(event.name)} as label`; + sb.where.eventName = `name = ${escape(event.name)}`; } sb.select.count = `count(*) as count`; @@ -64,10 +65,10 @@ export function getChartSql({ const breakdown = breakdowns[0]!; if (breakdown) { const value = breakdown.name.startsWith('properties.') - ? `mapValues(mapExtractKeyLike(properties, '${breakdown.name - .replace(/^properties\./, '') - .replace('.*.', '.%.')}'))` - : breakdown.name; + ? `mapValues(mapExtractKeyLike(properties, ${escape( + breakdown.name.replace(/^properties\./, '').replace('.*.', '.%.') + )}))` + : escape(breakdown.name); sb.select.label = breakdown.name.startsWith('properties.') ? `arrayElement(${value}, 1) as label` : `${breakdown.name} as label`; @@ -120,32 +121,32 @@ export function getEventFiltersWhereClause(filters: IChartEventFilter[]) { if (value.length === 0) return; if (name.startsWith('properties.')) { - const whereFrom = `mapValues(mapExtractKeyLike(properties, '${name - .replace(/^properties\./, '') - .replace('.*.', '.%.')}'))`; + const whereFrom = `mapValues(mapExtractKeyLike(properties, ${escape( + name.replace(/^properties\./, '').replace('.*.', '.%.') + )}))`; switch (operator) { case 'is': { where[id] = `arrayExists(x -> ${value - .map((val) => `x = '${String(val).trim()}'`) + .map((val) => `x = ${escape(String(val).trim())}`) .join(' OR ')}, ${whereFrom})`; break; } case 'isNot': { where[id] = `arrayExists(x -> ${value - .map((val) => `x != '${String(val).trim()}'`) + .map((val) => `x != ${escape(String(val).trim())}`) .join(' OR ')}, ${whereFrom})`; break; } case 'contains': { where[id] = `arrayExists(x -> ${value - .map((val) => `x LIKE '%${String(val).trim()}%'`) + .map((val) => `x LIKE ${escape(`%${String(val).trim()}%`)}`) .join(' OR ')}, ${whereFrom})`; break; } case 'doesNotContain': { where[id] = `arrayExists(x -> ${value - .map((val) => `x NOT LIKE '%${String(val).trim()}%'`) + .map((val) => `x NOT LIKE ${escape(`%${String(val).trim()}%`)}`) .join(' OR ')}, ${whereFrom})`; break; } @@ -154,25 +155,27 @@ export function getEventFiltersWhereClause(filters: IChartEventFilter[]) { switch (operator) { case 'is': { where[id] = `${name} IN (${value - .map((val) => `'${String(val).trim()}'`) + .map((val) => escape(String(val).trim())) .join(', ')})`; break; } case 'isNot': { where[id] = `${name} NOT IN (${value - .map((val) => `'${String(val).trim()}'`) + .map((val) => escape(String(val).trim())) .join(', ')})`; break; } case 'contains': { where[id] = value - .map((val) => `${name} LIKE '%${String(val).trim()}%'`) + .map((val) => `${name} LIKE ${escape(`%${String(val).trim()}%`)}`) .join(' OR '); break; } case 'doesNotContain': { where[id] = value - .map((val) => `${name} NOT LIKE '%${String(val).trim()}%'`) + .map( + (val) => `${name} NOT LIKE ${escape(`%${String(val).trim()}%`)}` + ) .join(' OR '); break; } diff --git a/packages/db/src/services/event.service.ts b/packages/db/src/services/event.service.ts index 1ebcdb76..203a9522 100644 --- a/packages/db/src/services/event.service.ts +++ b/packages/db/src/services/event.service.ts @@ -1,4 +1,5 @@ import { omit, uniq } from 'ramda'; +import { escape } from 'sqlstring'; import { v4 as uuid } from 'uuid'; import { randomSplitName, toDots } from '@openpanel/common'; @@ -261,15 +262,15 @@ export async function getEventList({ sb.limit = take; sb.offset = Math.max(0, (cursor ?? 0) * take); - sb.where.projectId = `project_id = '${projectId}'`; + sb.where.projectId = `project_id = ${escape(projectId)}`; if (profileId) { - sb.where.deviceId = `device_id IN (SELECT device_id as did FROM openpanel.events WHERE profile_id = '${profileId}' group by did)`; + sb.where.deviceId = `device_id IN (SELECT device_id as did FROM openpanel.events WHERE profile_id = ${escape(profileId)} group by did)`; } if (events && events.length > 0) { sb.where.events = `name IN (${join( - events.map((n) => `'${n}'`), + events.map((event) => escape(event)), ',' )})`; } @@ -297,14 +298,14 @@ export async function getEventsCount({ filters, }: Omit) { const { sb, getSql, join } = createSqlBuilder(); - sb.where.projectId = `project_id = '${projectId}'`; + sb.where.projectId = `project_id = ${escape(projectId)}`; if (profileId) { - sb.where.profileId = `profile_id = '${profileId}'`; + sb.where.profileId = `profile_id = ${escape(profileId)}`; } if (events && events.length > 0) { sb.where.events = `name IN (${join( - events.map((n) => `'${n}'`), + events.map((event) => escape(event)), ',' )})`; } diff --git a/packages/db/src/services/profile.service.ts b/packages/db/src/services/profile.service.ts index 3fe47ee4..5cfa16f3 100644 --- a/packages/db/src/services/profile.service.ts +++ b/packages/db/src/services/profile.service.ts @@ -1,3 +1,5 @@ +import { escape } from 'sqlstring'; + import { toDots, toObject } from '@openpanel/common'; import type { IChartEventFilter } from '@openpanel/validation'; @@ -11,7 +13,7 @@ export async function getProfileById(id: string) { } const [profile] = await chQuery( - `SELECT *, created_at as max_created_at FROM profiles WHERE id = '${id}' ORDER BY created_at DESC LIMIT 1` + `SELECT *, created_at as max_created_at FROM profiles WHERE id = ${escape(id)} ORDER BY created_at DESC LIMIT 1` ); if (!profile) { @@ -53,7 +55,7 @@ export async function getProfiles({ ids }: GetProfilesOptions) { `SELECT ${getProfileSelectFields()} FROM profiles - WHERE id IN (${ids.map((id) => `'${id}'`).join(',')}) + WHERE id IN (${ids.map((id) => escape(id)).join(',')}) GROUP BY id ` ); @@ -66,7 +68,7 @@ function getProfileInnerSelect(projectId: string) { ${getProfileSelectFields()} FROM profiles GROUP BY id - HAVING project_id = '${projectId}')`; + HAVING project_id = ${escape(projectId)})`; } export async function getProfileList({ @@ -120,7 +122,7 @@ export async function getProfilesByExternalId( ${getProfileSelectFields()} FROM profiles GROUP BY id - HAVING project_id = '${projectId}' AND external_id = '${externalId}' + HAVING project_id = ${escape(projectId)} AND external_id = ${escape(externalId)} ` ); @@ -192,7 +194,7 @@ export async function upsertProfile({ projectId, }: IServiceUpsertProfile) { const [profile] = await chQuery( - `SELECT * FROM profiles WHERE id = '${id}' AND project_id = '${projectId}' ORDER BY created_at DESC LIMIT 1` + `SELECT * FROM profiles WHERE id = ${escape(id)} AND project_id = ${escape(projectId)} ORDER BY created_at DESC LIMIT 1` ); await ch.insert({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 36e8af58..ceea9fd7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,6 +65,9 @@ importers: sharp: specifier: ^0.33.2 version: 0.33.2 + sqlstring: + specifier: ^2.3.3 + version: 2.3.3 ua-parser-js: specifier: ^1.0.37 version: 1.0.37 @@ -90,6 +93,9 @@ importers: '@types/ramda': specifier: ^0.29.6 version: 0.29.10 + '@types/sqlstring': + specifier: ^2.3.2 + version: 2.3.2 '@types/ua-parser-js': specifier: ^0.7.39 version: 0.7.39 @@ -345,6 +351,9 @@ importers: sonner: specifier: ^1.4.0 version: 1.4.0(react-dom@18.2.0)(react@18.2.0) + sqlstring: + specifier: ^2.3.3 + version: 2.3.3 superjson: specifier: ^1.13.3 version: 1.13.3 @@ -397,6 +406,9 @@ importers: '@types/request-ip': specifier: ^0.0.41 version: 0.0.41 + '@types/sqlstring': + specifier: ^2.3.2 + version: 2.3.2 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 version: 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) @@ -776,6 +788,9 @@ importers: ramda: specifier: ^0.29.1 version: 0.29.1 + sqlstring: + specifier: ^2.3.3 + version: 2.3.3 uuid: specifier: ^9.0.1 version: 9.0.1 @@ -795,6 +810,9 @@ importers: '@types/ramda': specifier: ^0.29.6 version: 0.29.10 + '@types/sqlstring': + specifier: ^2.3.2 + version: 2.3.2 '@types/uuid': specifier: ^9.0.8 version: 9.0.8 @@ -7012,6 +7030,10 @@ packages: '@types/mime': 3.0.4 '@types/node': 18.19.17 + /@types/sqlstring@2.3.2: + resolution: {integrity: sha512-lVRe4Iz9UNgiHelKVo8QlC8fb5nfY8+p+jNQNE+UVsuuVlQnWhyWmQ/wF5pE8Ys6TdjfVpqTG9O9i2vi6E0+Sg==} + dev: true + /@types/stack-trace@0.0.29: resolution: {integrity: sha512-TgfOX+mGY/NyNxJLIbDWrO9DjGoVSW9+aB8H2yy1fy32jsvxijhmyJI9fDFgvz3YP4lvJaq9DzdR/M1bOgVc9g==} dev: false @@ -15958,6 +15980,11 @@ packages: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} dev: false + /sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + dev: false + /ssri@8.0.1: resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==} engines: {node: '>= 8'}