v0.9.26: abuse prevention, project files by waleedlatif1 · Pull Request #9155 · simstudioai/sim · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f542992
improvement(library): refresh license comparison and Zapier alternati…
waleedlatif1 Oct 16, 2026
854dc6a
fix(files): answer 499 instead of 500 when a client disconnects mid-s…
waleedlatif1 Oct 16, 2026
7561e0f
fix(trigger): complete async workflow runs whose failure is already r…
waleedlatif1 Oct 16, 2026
e5feb08
fix(execution): end runs quietly when their workspace is deleted mid-…
waleedlatif1 Oct 16, 2026
282524f
revert(search): restore the v0.9.23 Jira text search (#9148)
waleedlatif1 Oct 16, 2026
c3f2b30
fix(trigger): do not alert on runs whose workspace was deleted mid-ru…
waleedlatif1 Oct 16, 2026
84f41ea
fix(execution): stop in-flight workflow runs when an account is delet…
waleedlatif1 Oct 16, 2026
99ed5fb
feat(project-files): let Chat read and write Project files (#9034)
mzxchandra Oct 16, 2026
678c1a4
fix(account-deletion): durably purge a deleted account's storage with…
waleedlatif1 Oct 17, 2026
d44cee4
improvement(landing): serve the homepage as Markdown when clients ask…
waleedlatif1 Oct 17, 2026
f4daf59
fix(billing): stop account deletion from resetting the free allowance…
waleedlatif1 Oct 17, 2026
2af5443
chore(ci): remove the flaky-test issue workflow (#9156)
waleedlatif1 Oct 17, 2026
e16fd83
fix(search): count two-letter query terms when ranking across sources…
waleedlatif1 Oct 17, 2026
99b6de7
fix(account-deletion): resume the workspace storage sweep from its sa…
waleedlatif1 Oct 17, 2026
962786f
fix(chat): open and close Project file tabs by their owner (#9160)
waleedlatif1 Oct 17, 2026
96555d3
fix(chat): treat browser tab metadata as untrusted and bound attachme…
waleedlatif1 Oct 17, 2026
c5c1a1e
feat(data-drains): add Microsoft Sentinel ingestion (#9162)
waleedlatif1 Oct 17, 2026
b7394be
fix: release-review follow-ups for account removal (workflow-state re…
waleedlatif1 Oct 17, 2026
0a140b4
feat(oracle-fusion): add core financial business operations (#9165)
waleedlatif1 Oct 17, 2026
5f49cbf
feat(databricks): expand API coverage and preserve paginated results …
waleedlatif1 Oct 17, 2026
477028d
fix(chat): release-audit follow-ups for Project file tabs and sandbox…
waleedlatif1 Oct 17, 2026
c05168a
docs(library): update best-zapier-alternatives (#9170)
waleedlatif1 Oct 17, 2026
4e8919f
feat(library): 6 Best Make.com Alternatives for AI Agents in 2026 (#9…
waleedlatif1 Oct 17, 2026
c9527c5
feat(library): How Much Does It Cost to Build and Run an AI Agent? (#…
waleedlatif1 Oct 17, 2026
f40e321
feat(library): 6 Best Relevance AI Alternatives for Flexible AI Agent…
waleedlatif1 Oct 17, 2026
3dc0283
fix(credentials): reject managed OAuth kind mismatches before resolut…
waleedlatif1 Oct 17, 2026
ca43d9d
docs(library): update apache-2-0-vs-fair-code (#9174)
waleedlatif1 Oct 17, 2026
5848bd5
fix(databricks): preserve mutation intent and complete task outputs (…
waleedlatif1 Oct 17, 2026
2d0ac79
fix(salesforce): pin instance URL to Salesforce domains and guard upd…
waleedlatif1 Oct 17, 2026
f79d322
fix(auth): revoke sessions and OAuth grants on password reset (#9178)
waleedlatif1 Oct 17, 2026
2fe1e88
fix(executor): read files in Copilot-started runs and replay repeated…
waleedlatif1 Oct 17, 2026
3dcf9fb
fix(copilot): re-derive the turn's secret authority on the in-band to…
waleedlatif1 Oct 17, 2026
22a0333
fix(auth): end OAuth grants on SCIM address change and add sign out o…
waleedlatif1 Oct 17, 2026
d195710
fix(resume): require write access to resume a paused run on the legac…
waleedlatif1 Oct 17, 2026
9f7ca62
improvement(computer-use): roll out computer use per organization (#9…
waleedlatif1 Oct 17, 2026
760594b
fix(logs): remove the client log-persistence route that let a run's a…
waleedlatif1 Oct 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix(salesforce): pin instance URL to Salesforce domains and guard upd…
…ate-custom-field egress (#9177)

* fix(salesforce): pin instance URL to Salesforce domains and guard update-custom-field egress

* fix(security): check vendor-hosted URL scheme on the parsed URL and pass abort signal to DNS validation

* test(salesforce): cover private-address DNS refusal in update-custom-field
  • Loading branch information
waleedlatif1 authored Oct 17, 2026
commit 2d0ac795bc9190cfbea1036c45dc46c5af76918c
44 changes: 42 additions & 2 deletions apps/sim/lib/core/security/input-validation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'

const { mockResolveHostAddresses } = vi.hoisted(() => ({
mockResolveHostAddresses: vi.fn(),
}))

vi.mock('@sim/security/dns', () => ({
resolveHostAddresses: mockResolveHostAddresses,
preferIpv4: (addresses: string[]) => addresses[0],
}))

import { executeSalesforceUpdateCustomFieldOperation } from '@/lib/internal/salesforce/operations/update-custom-field'

const PARAMS = {
Expand All @@ -14,14 +24,29 @@ describe('salesforce update custom field operation', () => {
})

it('rejects a field ID that could escape the CustomField path before provider work', async () => {
const fetchMock = vi.mocked(fetch)

await expect(
executeSalesforceUpdateCustomFieldOperation({
...PARAMS,
fieldId: '../CustomObject',
} as never)
).rejects.toThrow('Field ID must be a 15- or 18-character Salesforce record ID')
expect(fetchMock).not.toHaveBeenCalled()
})

it('refuses an instance URL off Salesforce', async () => {
await expect(
executeSalesforceUpdateCustomFieldOperation({
...PARAMS,
instanceUrl: 'http://169.254.169.254/latest/meta-data/iam/security-credentials/?x=',
} as never)
).rejects.toThrow('Salesforce instance URL must be on a Salesforce domain')
})

it('refuses a Salesforce host that resolves to a private address before sending the token', async () => {
mockResolveHostAddresses.mockResolvedValue({ addresses: ['10.0.0.5'] })

await expect(executeSalesforceUpdateCustomFieldOperation(PARAMS as never)).rejects.toThrow(
'instanceUrl resolves to a private or reserved address'
)
expect(fetch).not.toHaveBeenCalled()
})
})
26 changes: 22 additions & 4 deletions apps/sim/lib/internal/salesforce/operations/update-custom-field.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { createLogger } from '@sim/logger'
import { isRecordLike } from '@sim/utils/object'
import {
MAX_JSON_API_RESPONSE_BYTES,
secureFetchWithPinnedIP,
validateUrlWithDNS,
} from '@/lib/core/security/input-validation.server'
import type { InternalToolOperationImplementation } from '@/lib/internal/tool-operations/types'
import type {
SalesforceUpdateCustomFieldParams,
Expand Down Expand Up @@ -28,8 +33,22 @@ export const executeSalesforceUpdateCustomFieldOperation: InternalToolOperationI
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
const validation = await validateUrlWithDNS(url, 'instanceUrl', 'configuredEndpoint', { signal })
signal?.throwIfAborted()
if (!validation.isValid) {
throw new Error(validation.error || 'Invalid Salesforce instance URL')
}
const fetchOptions = {
profile: 'configuredEndpoint',
headers,
maxResponseBytes: MAX_JSON_API_RESPONSE_BYTES,
signal,
} as const

const readResponse = await fetch(url, { headers, signal })
const readResponse = await secureFetchWithPinnedIP(url, validation.resolvedIP, {
...fetchOptions,
method: 'GET',
})
let existing: unknown
try {
existing = await readResponse.json()
Expand All @@ -56,11 +75,10 @@ export const executeSalesforceUpdateCustomFieldOperation: InternalToolOperationI

const metadata = mergeCustomFieldMetadata(existing.Metadata, params)

const patchResponse = await fetch(url, {
const patchResponse = await secureFetchWithPinnedIP(url, validation.resolvedIP, {
...fetchOptions,
method: 'PATCH',
headers,
body: JSON.stringify({ Metadata: metadata }),
signal,
})
if (!patchResponse.ok) {
const errorData = await patchResponse.json().catch(() => {
Expand Down
24 changes: 24 additions & 0 deletions apps/sim/tools/salesforce/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,28 @@ describe('getInstanceUrl', () => {
expect(() => getInstanceUrl()).toThrow('Salesforce instance URL is required')
expect(() => getInstanceUrl('not-a-jwt')).toThrow('Salesforce instance URL is required')
})

it('reduces a provided instance URL to its origin', () => {
expect(getInstanceUrl(undefined, `${ORG}/services/data?x=1`)).toBe(ORG)
})

it.each([
['cloud metadata', 'https://169.254.169.254/latest/meta-data/?x='],
['plain http on a Salesforce host', 'http://acme.my.salesforce.com'],
['a lookalike suffix', 'https://acme.my.salesforce.com.attacker.example'],
['a lookalike label', 'https://attackersalesforce.com'],
['a bare Salesforce domain', 'https://salesforce.com'],
['a login host', 'https://login.salesforce.com'],
['a non-URL', 'not a url'],
])('rejects a provided instance URL with %s', (_label, instanceUrl) => {
expect(() => getInstanceUrl(idTokenWith({ profile: `${ORG}/005` }), instanceUrl)).toThrow(
'Salesforce instance URL must be on a Salesforce domain'
)
})

it('skips an id token claim off Salesforce', () => {
expect(() => getInstanceUrl(idTokenWith({ profile: 'https://169.254.169.254/005' }))).toThrow(
'Salesforce instance URL is required'
)
})
})
34 changes: 19 additions & 15 deletions apps/sim/tools/salesforce/utils.ts
Loading