Skip to content

Commit 171a4c4

Browse files
committed
fix: address SSRF and token-leakage security vulnerabilities
- Azure TTS SSRF: validate region against /^[a-z][a-z0-9-]{1,30}[a-z0-9]$/ in both the contract (tts.ts) and runtime guard in synthesizeWithAzure, preventing user-supplied region from redirecting requests to arbitrary hosts - HubSpot token in logs: remove fullResponse from logger.info call; log only non-sensitive metadata (hub_id, hub_domain, user_id) instead of the full introspection response which included the access token - Wealthbox account takeover: replace hardcoded email with per-user identity by fetching /v1/users/me; fall back to token-derived stable identifier so distinct Wealthbox users no longer share the same email address - Shopify SSRF: apply shopifyShopDomainSchema (.myshopify.com allowlist) to shopDomain from cookie before using it to build the fetch URL
1 parent ae680af commit 171a4c4

4 files changed

Lines changed: 65 additions & 9 deletions

File tree

apps/sim/app/api/auth/oauth2/shopify/store/route.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { account } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { and, eq } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
6-
import { shopifyStoreCookieSchema } from '@/lib/api/contracts/oauth-connections'
6+
import {
7+
shopifyShopDomainSchema,
8+
shopifyStoreCookieSchema,
9+
} from '@/lib/api/contracts/oauth-connections'
710
import { getSession } from '@/lib/auth'
811
import { getBaseUrl } from '@/lib/core/utils/urls'
912
import { isSameOrigin } from '@/lib/core/utils/validation'
@@ -38,6 +41,11 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
3841
}
3942
const { accessToken, shopDomain, scope, returnUrl } = parsedCookies.data
4043

44+
if (!shopifyShopDomainSchema.safeParse(shopDomain).success) {
45+
logger.error('Invalid shop domain format in cookie', { shopDomain })
46+
return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_invalid_domain`)
47+
}
48+
4149
const shopResponse = await fetch(`https://${shopDomain}/admin/api/2024-10/shop.json`, {
4250
headers: {
4351
'X-Shopify-Access-Token': accessToken,

apps/sim/app/api/tools/tts/unified/route.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,13 @@ async function synthesizeWithAzure(
659659
throw new Error('text and apiKey are required for Azure TTS')
660660
}
661661

662+
const AZURE_REGION_RE = /^[a-z][a-z0-9-]{1,30}[a-z0-9]$/
663+
if (!AZURE_REGION_RE.test(region)) {
664+
throw new Error(
665+
'Invalid Azure region: must match /^[a-z][a-z0-9-]{1,30}[a-z0-9]$/ (e.g. eastus, westeurope)'
666+
)
667+
}
668+
662669
let ssml = `<speak version='1.0' xml:lang='en-US' xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts"><voice name='${voiceId}'>`
663670

664671
if (style) {

apps/sim/lib/api/contracts/tools/media/tts.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,13 @@ export const ttsUnifiedToolBodySchema = z
5454
volumeGainDb: z.coerce.number().optional(),
5555
sampleRateHertz: z.coerce.number().optional(),
5656
effectsProfileId: z.array(z.string()).optional(),
57-
region: z.string().optional(),
57+
region: z
58+
.string()
59+
.regex(
60+
/^[a-z][a-z0-9-]{1,30}[a-z0-9]$/,
61+
'region must be a valid Azure region identifier (e.g. eastus, westeurope)'
62+
)
63+
.optional(),
5864
rate: z.string().optional(),
5965
styleDegree: z.coerce.number().optional(),
6066
role: z.string().optional(),

apps/sim/lib/auth/auth.ts

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1615,17 +1615,51 @@ export const auth = betterAuth({
16151615
scopes: getCanonicalScopesForProvider('wealthbox'),
16161616
responseType: 'code',
16171617
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/wealthbox`,
1618-
getUserInfo: async (_tokens) => {
1618+
getUserInfo: async (tokens) => {
16191619
try {
1620-
logger.info('Creating Wealthbox user profile from token data')
1620+
logger.info('Fetching Wealthbox user profile')
1621+
1622+
const response = await fetch('https://api.crmworkspace.com/v1/users/me', {
1623+
headers: {
1624+
ACCESS_TOKEN: tokens.accessToken,
1625+
},
1626+
})
16211627

1622-
const uniqueId = 'wealthbox-user'
16231628
const now = new Date()
16241629

1630+
if (response.ok) {
1631+
const data = await response.json()
1632+
const userId = data.id?.toString()
1633+
const email =
1634+
data.email && typeof data.email === 'string'
1635+
? data.email
1636+
: `wealthbox-${userId}@wealthbox.user`
1637+
const name = data.name || data.full_name || data.username || 'Wealthbox User'
1638+
1639+
return {
1640+
id: `wealthbox-${userId}-${generateId()}`,
1641+
name,
1642+
email,
1643+
emailVerified: false,
1644+
createdAt: now,
1645+
updatedAt: now,
1646+
}
1647+
}
1648+
1649+
// Fallback: derive a stable per-token identifier from the access token
1650+
// so that each Wealthbox user gets a unique account rather than all
1651+
// sharing the same hardcoded email.
1652+
logger.warn(
1653+
'Wealthbox user info fetch failed, falling back to token-derived identity',
1654+
{
1655+
status: response.status,
1656+
}
1657+
)
1658+
const tokenHash = Buffer.from(tokens.accessToken).toString('base64').slice(0, 24)
16251659
return {
1626-
id: `${uniqueId}-${generateId()}`,
1660+
id: `wealthbox-${tokenHash}-${generateId()}`,
16271661
name: 'Wealthbox User',
1628-
email: `${uniqueId}@wealthbox.user`,
1662+
email: `wealthbox-${tokenHash}@wealthbox.user`,
16291663
emailVerified: false,
16301664
createdAt: now,
16311665
updatedAt: now,
@@ -1730,11 +1764,12 @@ export const auth = betterAuth({
17301764
}
17311765

17321766
logger.info('HubSpot token metadata response:', {
1767+
hubId: data.hub_id,
1768+
hubDomain: data.hub_domain,
1769+
userId: data.user_id,
17331770
hasScopes: !!data.scopes,
17341771
scopesType: typeof data.scopes,
17351772
scopesIsArray: Array.isArray(data.scopes),
1736-
scopesValue: data.scopes,
1737-
fullResponse: data,
17381773
})
17391774

17401775
return {

0 commit comments

Comments
 (0)