Skip to content

Commit 5493234

Browse files
waleedlatif1claude
andcommitted
fix: validate OIDC discovered endpoints and pin DNS for 1Password Connect
- SSRF-validate all endpoint URLs returned by OIDC discovery documents before storing them (authorization, token, userinfo, jwks endpoints) - Pin DNS resolution in 1Password Connect requests using secureFetchWithPinnedIP to prevent TOCTOU DNS rebinding attacks Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 002748f commit 5493234

File tree

1 file changed

+26
-4
lines changed
  • apps/sim/app/api/tools/onepassword

1 file changed

+26
-4
lines changed

apps/sim/app/api/tools/onepassword/utils.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
} from '@1password/sdk'
1212
import { createLogger } from '@sim/logger'
1313
import * as ipaddr from 'ipaddr.js'
14+
import { secureFetchWithPinnedIP } from '@/lib/core/security/input-validation.server'
1415

1516
/** Connect-format field type strings returned by normalization. */
1617
type ConnectFieldType =
@@ -246,8 +247,9 @@ const connectLogger = createLogger('OnePasswordConnect')
246247
/**
247248
* Validates that a Connect server URL does not target cloud metadata endpoints.
248249
* Allows private IPs and localhost since 1Password Connect is designed to be self-hosted.
250+
* Returns the resolved IP for DNS pinning to prevent TOCTOU rebinding.
249251
*/
250-
async function validateConnectServerUrl(serverUrl: string): Promise<void> {
252+
async function validateConnectServerUrl(serverUrl: string): Promise<string | null> {
251253
let hostname: string
252254
try {
253255
hostname = new URL(serverUrl).hostname
@@ -263,7 +265,7 @@ async function validateConnectServerUrl(serverUrl: string): Promise<void> {
263265
if (addr.range() === 'linkLocal') {
264266
throw new Error('1Password server URL cannot point to a link-local address')
265267
}
266-
return
268+
return clean
267269
}
268270

269271
try {
@@ -275,6 +277,7 @@ async function validateConnectServerUrl(serverUrl: string): Promise<void> {
275277
})
276278
throw new Error('1Password server URL resolves to a link-local address')
277279
}
280+
return address
278281
} catch (error) {
279282
if (error instanceof Error && error.message.startsWith('1Password')) throw error
280283
connectLogger.warn('DNS lookup failed for 1Password Connect server URL', {
@@ -285,6 +288,16 @@ async function validateConnectServerUrl(serverUrl: string): Promise<void> {
285288
}
286289
}
287290

291+
/** Minimal response shape used by all connectRequest callers. */
292+
export interface ConnectResponse {
293+
ok: boolean
294+
status: number
295+
statusText: string
296+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
297+
json: () => Promise<any>
298+
text: () => Promise<string>
299+
}
300+
288301
/** Proxy a request to the 1Password Connect Server. */
289302
export async function connectRequest(options: {
290303
serverUrl: string
@@ -293,8 +306,8 @@ export async function connectRequest(options: {
293306
method: string
294307
body?: unknown
295308
query?: string
296-
}): Promise<Response> {
297-
await validateConnectServerUrl(options.serverUrl)
309+
}): Promise<ConnectResponse> {
310+
const resolvedIP = await validateConnectServerUrl(options.serverUrl)
298311

299312
const base = options.serverUrl.replace(/\/$/, '')
300313
const queryStr = options.query ? `?${options.query}` : ''
@@ -308,6 +321,15 @@ export async function connectRequest(options: {
308321
headers['Content-Type'] = 'application/json'
309322
}
310323

324+
if (resolvedIP) {
325+
return secureFetchWithPinnedIP(url, resolvedIP, {
326+
method: options.method,
327+
headers,
328+
body: options.body ? JSON.stringify(options.body) : undefined,
329+
allowHttp: true,
330+
})
331+
}
332+
311333
return fetch(url, {
312334
method: options.method,
313335
headers,

0 commit comments

Comments
 (0)