@@ -11,6 +11,7 @@ import type {
1111} from '@1password/sdk'
1212import { createLogger } from '@sim/logger'
1313import * 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. */
1617type 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. */
289302export 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