11import * as http from 'node:http' ;
22import { buffer } from 'node:stream/consumers' ;
3- import { debug , dsnFromString , getEnvelopeEndpointWithUrlEncodedAuth } from '@sentry/core' ;
3+ import {
4+ consoleSandbox ,
5+ debug ,
6+ type DsnComponents ,
7+ dsnToString ,
8+ getEnvelopeEndpointWithUrlEncodedAuth ,
9+ makeDsn ,
10+ } from '@sentry/core' ;
411import { DEBUG_BUILD } from './debug-build' ;
512
613/**
@@ -94,6 +101,19 @@ export class AwsLambdaExtension {
94101 * Starts the Sentry tunnel.
95102 */
96103 public startSentryTunnel ( ) : void {
104+ const allowedDsnComponents = getSentryDSNFromEnv ( ) ;
105+
106+ if ( ! allowedDsnComponents ) {
107+ consoleSandbox ( ( ) => {
108+ // eslint-disable-next-line no-console
109+ console . warn (
110+ 'Sentry Lambda extension: SENTRY_DSN is not set or is invalid. The /envelope tunnel will forward ' +
111+ 'any DSN in the envelope header without allowlist validation. Set SENTRY_DSN to the same DSN as ' +
112+ 'your SDK to restrict outbound requests.' ,
113+ ) ;
114+ } ) ;
115+ }
116+
97117 const server = http . createServer ( async ( req , res ) => {
98118 if ( req . method === 'POST' && req . url ?. startsWith ( '/envelope' ) ) {
99119 try {
@@ -104,12 +124,30 @@ export class AwsLambdaExtension {
104124 const envelope = new TextDecoder ( ) . decode ( envelopeBytes ) ;
105125 const piece = envelope . split ( '\n' ) [ 0 ] ;
106126 const header = JSON . parse ( piece || '{}' ) as { dsn ?: string } ;
107- if ( ! header . dsn ) {
108- throw new Error ( 'DSN is not set' ) ;
127+ const envelopeDsn = header . dsn ;
128+ if ( ! envelopeDsn ) {
129+ res . writeHead ( 400 , { 'Content-Type' : 'application/json' } ) ;
130+ res . end ( JSON . stringify ( { error : 'Invalid envelope: missing DSN' } ) ) ;
131+ return ;
132+ }
133+
134+ // When SENTRY_DSN is set, same allowlist check as handleTunnelRequest in @sentry/core (SSRF protection).
135+ // If not set, we allow any DSN (but warn about this once, above)
136+ if ( allowedDsnComponents ) {
137+ if ( dsnToString ( allowedDsnComponents ) !== envelopeDsn ) {
138+ DEBUG_BUILD &&
139+ debug . warn ( `Sentry Lambda extension tunnel: rejected request with unauthorized DSN (${ envelopeDsn } )` ) ;
140+ res . writeHead ( 403 , { 'Content-Type' : 'application/json' } ) ;
141+ res . end ( JSON . stringify ( { error : 'DSN not allowed' } ) ) ;
142+ return ;
143+ }
109144 }
110- const dsn = dsnFromString ( header . dsn ) ;
145+
146+ const dsn = allowedDsnComponents || makeDsn ( envelopeDsn ) ;
111147 if ( ! dsn ) {
112- throw new Error ( 'Invalid DSN' ) ;
148+ res . writeHead ( 403 , { 'Content-Type' : 'application/json' } ) ;
149+ res . end ( JSON . stringify ( { error : 'Invalid DSN' } ) ) ;
150+ return ;
113151 }
114152 const upstreamSentryUrl = getEnvelopeEndpointWithUrlEncodedAuth ( dsn ) ;
115153
@@ -143,3 +181,20 @@ export class AwsLambdaExtension {
143181 } ) ;
144182 }
145183}
184+
185+ /**
186+ * DSN components allowed for the Lambda extension `/envelope` tunnel, derived from `SENTRY_DSN`.
187+ *
188+ * Exported only for testing purposes.
189+ */
190+ export function getSentryDSNFromEnv ( ) : DsnComponents | undefined {
191+ const raw = process . env . SENTRY_DSN ?. trim ( ) ;
192+ if ( ! raw ) {
193+ return undefined ;
194+ }
195+ const components = makeDsn ( raw ) ;
196+ if ( ! components ) {
197+ return undefined ;
198+ }
199+ return components ;
200+ }
0 commit comments