11import https from 'https' ;
22import http from 'http' ;
3+ import dns from 'dns' ;
34import { createLogger , format , Logger , transports } from 'winston' ;
45import { WebhookDelivery } from '../types/template' ;
56
@@ -13,6 +14,36 @@ const DELIVERY_TIMEOUT_MS = 10000;
1314 */
1415const HTTP_ERROR_STATUS = 400 ;
1516
17+ /**
18+ * Checks whether an IPv4 or IPv6 address belongs to a private/reserved range.
19+ * Blocks loopback, link-local, RFC1918, metadata IPs and IPv6 equivalents.
20+ */
21+ function isPrivateIP ( ip : string ) : boolean {
22+ const parts = ip . split ( '.' ) . map ( Number ) ;
23+
24+ if ( parts . length === 4 && parts . every ( ( p ) => p >= 0 && p <= 255 ) ) {
25+ return (
26+ parts [ 0 ] === 127 ||
27+ parts [ 0 ] === 10 ||
28+ parts [ 0 ] === 0 ||
29+ ( parts [ 0 ] === 172 && parts [ 1 ] >= 16 && parts [ 1 ] <= 31 ) ||
30+ ( parts [ 0 ] === 192 && parts [ 1 ] === 168 ) ||
31+ ( parts [ 0 ] === 169 && parts [ 1 ] === 254 ) ||
32+ ( parts [ 0 ] === 100 && parts [ 1 ] >= 64 && parts [ 1 ] <= 127 )
33+ ) ;
34+ }
35+
36+ const lower = ip . toLowerCase ( ) ;
37+
38+ return (
39+ lower === '::1' ||
40+ lower . startsWith ( 'fe80' ) ||
41+ lower . startsWith ( 'fc' ) ||
42+ lower . startsWith ( 'fd' ) ||
43+ lower === '::' )
44+ ;
45+ }
46+
1647/**
1748 * Deliverer sends JSON POST requests to external webhook endpoints
1849 */
@@ -45,6 +76,35 @@ export default class WebhookDeliverer {
4576 public async deliver ( endpoint : string , delivery : WebhookDelivery ) : Promise < void > {
4677 const body = JSON . stringify ( delivery ) ;
4778 const url = new URL ( endpoint ) ;
79+
80+ if ( url . protocol !== 'https:' && url . protocol !== 'http:' ) {
81+ this . logger . log ( 'error' , `Webhook blocked — unsupported protocol: ${ url . protocol } for ${ endpoint } ` ) ;
82+
83+ return ;
84+ }
85+
86+ const hostname = url . hostname ;
87+
88+ if ( isPrivateIP ( hostname ) ) {
89+ this . logger . log ( 'error' , `Webhook blocked — private IP in URL: ${ endpoint } ` ) ;
90+
91+ return ;
92+ }
93+
94+ try {
95+ const { address } = await dns . promises . lookup ( hostname ) ;
96+
97+ if ( isPrivateIP ( address ) ) {
98+ this . logger . log ( 'error' , `Webhook blocked — ${ hostname } resolves to private IP ${ address } ` ) ;
99+
100+ return ;
101+ }
102+ } catch ( e ) {
103+ this . logger . log ( 'error' , `Webhook blocked — DNS lookup failed for ${ hostname } : ${ ( e as Error ) . message } ` ) ;
104+
105+ return ;
106+ }
107+
48108 const transport = url . protocol === 'https:' ? https : http ;
49109
50110 return new Promise < void > ( ( resolve ) => {
0 commit comments