Skip to content

Commit a1e77ee

Browse files
fix(credentials): address review feedback on Atlassian SA
- SSRF: only accept *.atlassian.net / *.jira-dev.com hosts before fetching tenant_info, blocking probes against localhost/internal IPs - Confluence spaces selector: pull cloudId from the SA secret instead of calling accessible-resources, which 401s for scoped service-account tokens - Case-insensitive https?:// strip so HTTPS://team.atlassian.net normalizes correctly
1 parent 7ea0c43 commit a1e77ee

3 files changed

Lines changed: 39 additions & 4 deletions

File tree

apps/sim/app/api/auth/atlassian-service-account/route.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,30 @@ class DuplicateDisplayNameError extends Error {
5151
}
5252
}
5353

54+
/**
55+
* Atlassian Cloud sites are always served from `*.atlassian.net` (production)
56+
* or `*.jira-dev.com` (Atlassian's developer sandbox). Anything else is either
57+
* a typo (`atlassian.com`, `jira.com`), a Data Center hostname (which our
58+
* gateway URL doesn't support), or — worse — an attempt to point this
59+
* server-side fetch at internal infrastructure (`localhost`, `169.254.169.254`,
60+
* `*.corp`). Restricting to the public Atlassian Cloud suffixes blocks SSRF
61+
* at the boundary before any outbound request.
62+
*/
63+
const ATLASSIAN_CLOUD_HOST_REGEX =
64+
/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.(?:atlassian\.net|jira-dev\.com)$/i
65+
5466
function normalizeDomain(rawDomain: string): string {
55-
return rawDomain.replace(/^https?:\/\//, '').replace(/\/+$/, '')
67+
return rawDomain.replace(/^https?:\/\//i, '').replace(/\/+$/, '')
68+
}
69+
70+
function assertAtlassianCloudHost(domain: string): void {
71+
if (!ATLASSIAN_CLOUD_HOST_REGEX.test(domain)) {
72+
throw new AtlassianValidationError('site_not_found', 400, {
73+
step: 'host_validation',
74+
domain,
75+
reason: 'host is not an Atlassian Cloud site (expected *.atlassian.net)',
76+
})
77+
}
5678
}
5779

5880
/**
@@ -90,6 +112,8 @@ async function validateAtlassianServiceAccount(
90112
apiToken: string,
91113
domain: string
92114
): Promise<{ accountId: string; displayName: string; cloudId: string }> {
115+
assertAtlassianCloudHost(domain)
116+
93117
const tenantInfoRes = await fetch(`https://${domain}/_edge/tenant_info`, {
94118
headers: { Accept: 'application/json' },
95119
})

apps/sim/app/api/tools/confluence/selector-spaces/route.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import { authorizeCredentialUse } from '@/lib/auth/credential-access'
66
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
77
import { generateRequestId } from '@/lib/core/utils/request'
88
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
9-
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
9+
import { ATLASSIAN_SERVICE_ACCOUNT_PROVIDER_ID } from '@/lib/oauth/types'
10+
import {
11+
getAtlassianServiceAccountSecret,
12+
refreshAccessTokenIfNeeded,
13+
resolveOAuthAccountId,
14+
} from '@/app/api/auth/oauth/utils'
1015
import { getConfluenceCloudId } from '@/tools/confluence/utils'
1116
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
1217

@@ -55,7 +60,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
5560
)
5661
}
5762

58-
const cloudId = await getConfluenceCloudId(domain, accessToken)
63+
// Atlassian service-account scoped tokens cannot call accessible-resources, so we
64+
// pull cloudId from the encrypted secret instead of discovering it at runtime.
65+
const resolved = await resolveOAuthAccountId(credential)
66+
const cloudId =
67+
resolved?.providerId === ATLASSIAN_SERVICE_ACCOUNT_PROVIDER_ID && resolved.credentialId
68+
? (await getAtlassianServiceAccountSecret(resolved.credentialId)).cloudId
69+
: await getConfluenceCloudId(domain, accessToken)
5970

6071
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
6172
if (!cloudIdValidation.isValid) {

apps/sim/app/workspace/[workspaceId]/settings/components/integrations/atlassian-service-account-form.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ const FALLBACK_ERROR_MESSAGE = "We couldn't add this service account. Try again
4747
function normalizeDomain(raw: string): string {
4848
return raw
4949
.trim()
50-
.replace(/^https?:\/\//, '')
50+
.replace(/^https?:\/\//i, '')
5151
.replace(/\/+$/, '')
5252
}
5353

0 commit comments

Comments
 (0)