diff --git a/pkg/js/generated/go/libldap/ldap.go b/pkg/js/generated/go/libldap/ldap.go index da1b6b42f5..c7d19d3d2c 100644 --- a/pkg/js/generated/go/libldap/ldap.go +++ b/pkg/js/generated/go/libldap/ldap.go @@ -53,12 +53,13 @@ func init() { "FilterWorkstationTrustAccount": lib_ldap.FilterWorkstationTrustAccount, // Objects / Classes - "Client": lib_ldap.NewClient, - "Config": gojs.GetClassConstructor[lib_ldap.Config](&lib_ldap.Config{}), - "LdapAttributes": gojs.GetClassConstructor[lib_ldap.LdapAttributes](&lib_ldap.LdapAttributes{}), - "LdapEntry": gojs.GetClassConstructor[lib_ldap.LdapEntry](&lib_ldap.LdapEntry{}), - "Metadata": gojs.GetClassConstructor[lib_ldap.Metadata](&lib_ldap.Metadata{}), - "SearchResult": gojs.GetClassConstructor[lib_ldap.SearchResult](&lib_ldap.SearchResult{}), + "AuthenticateOptions": gojs.GetClassConstructor[lib_ldap.AuthenticateOptions](&lib_ldap.AuthenticateOptions{}), + "Client": lib_ldap.NewClient, + "Config": gojs.GetClassConstructor[lib_ldap.Config](&lib_ldap.Config{}), + "LdapAttributes": gojs.GetClassConstructor[lib_ldap.LdapAttributes](&lib_ldap.LdapAttributes{}), + "LdapEntry": gojs.GetClassConstructor[lib_ldap.LdapEntry](&lib_ldap.LdapEntry{}), + "Metadata": gojs.GetClassConstructor[lib_ldap.Metadata](&lib_ldap.Metadata{}), + "SearchResult": gojs.GetClassConstructor[lib_ldap.SearchResult](&lib_ldap.SearchResult{}), }, ).Register() } diff --git a/pkg/js/generated/go/libmssql/mssql.go b/pkg/js/generated/go/libmssql/mssql.go index 06a336e952..03d3ebe944 100644 --- a/pkg/js/generated/go/libmssql/mssql.go +++ b/pkg/js/generated/go/libmssql/mssql.go @@ -19,8 +19,9 @@ func init() { // Var and consts // Objects / Classes - "MSSQLClient": gojs.GetClassConstructor[lib_mssql.MSSQLClient](&lib_mssql.MSSQLClient{}), - "MSSQLInfo": gojs.GetClassConstructor[lib_mssql.MSSQLInfo](&lib_mssql.MSSQLInfo{}), + "MSSQLClient": gojs.GetClassConstructor[lib_mssql.MSSQLClient](&lib_mssql.MSSQLClient{}), + "MSSQLInfo": gojs.GetClassConstructor[lib_mssql.MSSQLInfo](&lib_mssql.MSSQLInfo{}), + "MSSQLOptions": gojs.GetClassConstructor[lib_mssql.MSSQLOptions](&lib_mssql.MSSQLOptions{}), }, ).Register() } diff --git a/pkg/js/generated/go/liboracle/oracle.go b/pkg/js/generated/go/liboracle/oracle.go index e0d7a54740..65e2adaf77 100644 --- a/pkg/js/generated/go/liboracle/oracle.go +++ b/pkg/js/generated/go/liboracle/oracle.go @@ -21,6 +21,7 @@ func init() { // Objects / Classes "IsOracleResponse": gojs.GetClassConstructor[lib_oracle.IsOracleResponse](&lib_oracle.IsOracleResponse{}), "OracleClient": gojs.GetClassConstructor[lib_oracle.OracleClient](&lib_oracle.OracleClient{}), + "OracleOptions": gojs.GetClassConstructor[lib_oracle.OracleOptions](&lib_oracle.OracleOptions{}), }, ).Register() } diff --git a/pkg/js/generated/go/libpostgres/postgres.go b/pkg/js/generated/go/libpostgres/postgres.go index 592be64aad..4650d90e97 100644 --- a/pkg/js/generated/go/libpostgres/postgres.go +++ b/pkg/js/generated/go/libpostgres/postgres.go @@ -19,7 +19,8 @@ func init() { // Var and consts // Objects / Classes - "PGClient": gojs.GetClassConstructor[lib_postgres.PGClient](&lib_postgres.PGClient{}), + "PGClient": gojs.GetClassConstructor[lib_postgres.PGClient](&lib_postgres.PGClient{}), + "PostgresOptions": gojs.GetClassConstructor[lib_postgres.PostgresOptions](&lib_postgres.PostgresOptions{}), }, ).Register() } diff --git a/pkg/js/generated/go/libredis/redis.go b/pkg/js/generated/go/libredis/redis.go index 77f10adc4e..35ed1485df 100644 --- a/pkg/js/generated/go/libredis/redis.go +++ b/pkg/js/generated/go/libredis/redis.go @@ -15,16 +15,17 @@ func init() { module.Set( gojs.Objects{ // Functions - "Connect": lib_redis.Connect, - "GetServerInfo": lib_redis.GetServerInfo, - "GetServerInfoAuth": lib_redis.GetServerInfoAuth, - "IsAuthenticated": lib_redis.IsAuthenticated, - "RunLuaScript": lib_redis.RunLuaScript, + "Connect": lib_redis.Connect, //nolint:staticcheck // SA1019: retained for JS API compatibility + "ConnectWithOptions": lib_redis.ConnectWithOptions, + "GetServerInfo": lib_redis.GetServerInfo, + "GetServerInfoAuth": lib_redis.GetServerInfoAuth, + "IsAuthenticated": lib_redis.IsAuthenticated, + "RunLuaScript": lib_redis.RunLuaScript, // Var and consts // Objects / Classes - + "RedisOptions": gojs.GetClassConstructor[lib_redis.RedisOptions](&lib_redis.RedisOptions{}), }, ).Register() } diff --git a/pkg/js/generated/go/libsmb/smb.go b/pkg/js/generated/go/libsmb/smb.go index f6cbff1938..357501bb87 100644 --- a/pkg/js/generated/go/libsmb/smb.go +++ b/pkg/js/generated/go/libsmb/smb.go @@ -19,7 +19,8 @@ func init() { // Var and consts // Objects / Classes - "SMBClient": gojs.GetClassConstructor[lib_smb.SMBClient](&lib_smb.SMBClient{}), + "SMBClient": gojs.GetClassConstructor[lib_smb.SMBClient](&lib_smb.SMBClient{}), + "SMBOptions": gojs.GetClassConstructor[lib_smb.SMBOptions](&lib_smb.SMBOptions{}), }, ).Register() } diff --git a/pkg/js/generated/go/libssh/ssh.go b/pkg/js/generated/go/libssh/ssh.go index 672559426c..a6002585f2 100644 --- a/pkg/js/generated/go/libssh/ssh.go +++ b/pkg/js/generated/go/libssh/ssh.go @@ -19,7 +19,8 @@ func init() { // Var and consts // Objects / Classes - "SSHClient": gojs.GetClassConstructor[lib_ssh.SSHClient](&lib_ssh.SSHClient{}), + "SSHClient": gojs.GetClassConstructor[lib_ssh.SSHClient](&lib_ssh.SSHClient{}), + "SSHOptions": gojs.GetClassConstructor[lib_ssh.SSHOptions](&lib_ssh.SSHOptions{}), }, ).Register() } diff --git a/pkg/js/generated/go/libtelnet/telnet.go b/pkg/js/generated/go/libtelnet/telnet.go index 0c3672992e..04e5f8f6dc 100644 --- a/pkg/js/generated/go/libtelnet/telnet.go +++ b/pkg/js/generated/go/libtelnet/telnet.go @@ -35,6 +35,7 @@ func init() { "IsTelnetResponse": gojs.GetClassConstructor[lib_telnet.IsTelnetResponse](&lib_telnet.IsTelnetResponse{}), "TelnetClient": gojs.GetClassConstructor[lib_telnet.TelnetClient](&lib_telnet.TelnetClient{}), "TelnetInfoResponse": gojs.GetClassConstructor[lib_telnet.TelnetInfoResponse](&lib_telnet.TelnetInfoResponse{}), + "TelnetOptions": gojs.GetClassConstructor[lib_telnet.TelnetOptions](&lib_telnet.TelnetOptions{}), }, ).Register() } diff --git a/pkg/js/generated/go/libvnc/vnc.go b/pkg/js/generated/go/libvnc/vnc.go index 143808ea2d..283403e50b 100644 --- a/pkg/js/generated/go/libvnc/vnc.go +++ b/pkg/js/generated/go/libvnc/vnc.go @@ -22,6 +22,7 @@ func init() { // Objects / Classes "IsVNCResponse": gojs.GetClassConstructor[lib_vnc.IsVNCResponse](&lib_vnc.IsVNCResponse{}), "VNCClient": gojs.GetClassConstructor[lib_vnc.VNCClient](&lib_vnc.VNCClient{}), + "VNCOptions": gojs.GetClassConstructor[lib_vnc.VNCOptions](&lib_vnc.VNCOptions{}), }, ).Register() } diff --git a/pkg/js/generated/ts/ldap.ts b/pkg/js/generated/ts/ldap.ts index 02a10075b9..6511ac2e42 100644 --- a/pkg/js/generated/ts/ldap.ts +++ b/pkg/js/generated/ts/ldap.ts @@ -404,6 +404,14 @@ export class Client { } + /** + * AuthenticateWithOptions authenticates using a password or an NTLM hash. + */ + public AuthenticateWithOptions(opts: AuthenticateOptions): boolean { + return false; + } + + /** * AuthenticateWithNTLMHash authenticates with the ldap server using the given username and NTLM hash * @example @@ -507,6 +515,21 @@ export interface Config { +/** + * AuthenticateOptions defines credentials for LDAP authentication. Hash takes + * precedence over Password when both are provided. + */ +export interface AuthenticateOptions { + + Username?: string, + + Password?: string, + + Hash?: string, +} + + + /** * LdapAttributes represents all LDAP attributes of a particular * ldap entry diff --git a/pkg/js/generated/ts/mssql.ts b/pkg/js/generated/ts/mssql.ts index 7a570e9a83..251f7ad40d 100644 --- a/pkg/js/generated/ts/mssql.ts +++ b/pkg/js/generated/ts/mssql.ts @@ -46,6 +46,14 @@ export class MSSQLClient { } + /** + * ConnectWithOptions connects to MS SQL using the supplied connection options. + */ + public ConnectWithOptions(opts: MSSQLOptions): boolean | null { + return null; + } + + /** * IsMssql checks if the given host is running MS SQL database. * If the host is running MS SQL database, it returns true. @@ -95,6 +103,26 @@ export class MSSQLClient { +/** + * MSSQLOptions defines the connection options for an MS SQL database. + */ +export interface MSSQLOptions { + + Host?: string, + + Port?: number, + + Username?: string, + + Password?: string, + + DbName?: string, + + Timeout?: number, +} + + + /** * SQLResult Interface */ diff --git a/pkg/js/generated/ts/mysql.ts b/pkg/js/generated/ts/mysql.ts index bd9c7fe587..d4589c4c23 100644 --- a/pkg/js/generated/ts/mysql.ts +++ b/pkg/js/generated/ts/mysql.ts @@ -63,6 +63,14 @@ export class MySQLClient { } + /** + * ConnectWithOptions connects to MySQL using the supplied connection options. + */ + public ConnectWithOptions(opts: MySQLOptions): boolean | null { + return null; + } + + /** * returns MySQLInfo when fingerprint is successful * @example diff --git a/pkg/js/generated/ts/oracle.ts b/pkg/js/generated/ts/oracle.ts index 5701a4c513..469a679b24 100644 --- a/pkg/js/generated/ts/oracle.ts +++ b/pkg/js/generated/ts/oracle.ts @@ -41,6 +41,13 @@ export class OracleClient { return null; } + /** + * ConnectWithOptions connects to Oracle using the supplied connection options. + */ + public ConnectWithOptions(opts: OracleOptions): boolean | null { + return null; + } + /** * ConnectWithDSN connects to an Oracle database using a DSN string * @example @@ -96,6 +103,17 @@ export class OracleClient { } } +/** + * OracleOptions defines the connection options for an Oracle database. + */ +export interface OracleOptions { + Host?: string, + Port?: number, + ServiceName?: string, + Username?: string, + Password?: string, +} + /** * SQLResult Interface */ diff --git a/pkg/js/generated/ts/postgres.ts b/pkg/js/generated/ts/postgres.ts index 31efd44c33..26bc48f01e 100644 --- a/pkg/js/generated/ts/postgres.ts +++ b/pkg/js/generated/ts/postgres.ts @@ -46,6 +46,14 @@ export class PGClient { } + /** + * ConnectWithOptions connects to Postgres using the supplied connection options. + */ + public ConnectWithOptions(opts: PostgresOptions): boolean | null { + return null; + } + + /** * ExecuteQuery connects to Postgres database using given credentials and database name. * and executes a query on the db. @@ -84,6 +92,28 @@ export class PGClient { +/** + * PostgresOptions defines the connection options for a Postgres database. + */ +export interface PostgresOptions { + + Host?: string, + + Port?: number, + + Username?: string, + + Password?: string, + + DbName?: string, + + Timeout?: number, + + SSLMode?: string, +} + + + /** * SQLResult Interface */ diff --git a/pkg/js/generated/ts/redis.ts b/pkg/js/generated/ts/redis.ts index 2d7c4879c1..1a79f298f8 100644 --- a/pkg/js/generated/ts/redis.ts +++ b/pkg/js/generated/ts/redis.ts @@ -14,6 +14,43 @@ export function Connect(host: string, port: number, password: string): boolean | +/** + * ConnectWithOptions tries to connect to Redis using the supplied options. + * @example + * ```javascript + * const redis = require('nuclei/redis'); + * const opts = new redis.RedisOptions(); + * opts.Host = 'acme.com'; + * opts.Port = 6379; + * opts.Password = 'password'; + * opts.DB = 1; + * const connected = redis.ConnectWithOptions(opts); + * ``` + */ +export function ConnectWithOptions(opts: RedisOptions): boolean | null { + return null; +} + + + +/** + * RedisOptions defines the connection options for a Redis server. + */ +export interface RedisOptions { + + Host?: string, + + Port?: number, + + Password?: string, + + DB?: number, + + Timeout?: number, +} + + + /** * GetServerInfo returns the server info for a redis server * @example diff --git a/pkg/js/generated/ts/smb.ts b/pkg/js/generated/ts/smb.ts index 7fadfa24ee..f617471975 100644 --- a/pkg/js/generated/ts/smb.ts +++ b/pkg/js/generated/ts/smb.ts @@ -130,6 +130,26 @@ export class SMBClient { return null; } + /** + * ListSharesWithOptions lists SMB share names using SMBOptions. + * Supports domain, pass-the-hash, workstation, and SPN overrides. + * @example + * ```javascript + * const smb = require('nuclei/smb'); + * const client = new smb.SMBClient(); + * const opts = new smb.SMBOptions(); + * opts.Host = 'acme.com'; + * opts.Port = 445; + * opts.User = 'Administrator'; + * opts.Domain = 'ACME'; + * const shares = client.ListSharesWithOptions(opts); + * ``` + */ + public ListSharesWithOptions(opts: SMBOptions): string[] | null { + return null; + } + + /** * DetectSMBGhost tries to detect SMBGhost vulnerability * by using SMBv3 compression feature. @@ -174,6 +194,38 @@ export interface ProtocolInfo { +/** + * SMBOptions represents configuration for authenticated SMB operations. + * @example + * ```javascript + * const smb = require('nuclei/smb'); + * const opts = new smb.SMBOptions(); + * opts.Host = 'acme.com'; + * opts.Port = 445; + * opts.User = 'Administrator'; + * opts.Password = 'password'; + * opts.Domain = 'ACME'; + * ``` + */ +export interface SMBOptions { + + Host?: string, + + Port?: number, + + User?: string, + + Password?: string, + + Hash?: string, + + Domain?: string, + + Timeout?: number, +} + + + /** * HeaderLog Interface */ diff --git a/pkg/js/generated/ts/ssh.ts b/pkg/js/generated/ts/ssh.ts index 058df03c5b..9233955a7f 100644 --- a/pkg/js/generated/ts/ssh.ts +++ b/pkg/js/generated/ts/ssh.ts @@ -28,6 +28,27 @@ export class SSHClient { } + /** + * ConnectWithOptions tries to connect using the provided SSHOptions. + * Prefer this over Connect/ConnectWithKey when setting timeout, client version, + * or combining password and private key auth. + * @example + * ```javascript + * const ssh = require('nuclei/ssh'); + * const client = new ssh.SSHClient(); + * const opts = new ssh.SSHOptions(); + * opts.Host = 'acme.com'; + * opts.Port = 22; + * opts.User = 'username'; + * opts.Password = 'password'; + * const connected = client.ConnectWithOptions(opts); + * ``` + */ + public ConnectWithOptions(opts: SSHOptions): boolean | null { + return null; + } + + /** * Connect tries to connect to provided host and port * with provided username and password with ssh. @@ -124,6 +145,41 @@ export class SSHClient { +/** + * SSHOptions represents configuration options for an SSH connection. + * Use ConnectWithOptions when you need protocol-specific knobs beyond + * username/password (timeout, client version, private key). + * @example + * ```javascript + * const ssh = require('nuclei/ssh'); + * const opts = new ssh.SSHOptions(); + * opts.Host = 'acme.com'; + * opts.Port = 22; + * opts.User = 'username'; + * opts.Password = 'password'; + * opts.Timeout = 15; + * opts.ClientVersion = 'SSH-2.0-OpenSSH_8.9'; + * ``` + */ +export interface SSHOptions { + + Host?: string, + + Port?: number, + + User?: string, + + Password?: string, + + PrivateKey?: string, + + Timeout?: number, + + ClientVersion?: string, +} + + + /** * Algorithms Interface */ diff --git a/pkg/js/generated/ts/telnet.ts b/pkg/js/generated/ts/telnet.ts index 39513bd91b..45ec8385dc 100644 --- a/pkg/js/generated/ts/telnet.ts +++ b/pkg/js/generated/ts/telnet.ts @@ -39,6 +39,13 @@ export class TelnetClient { return false; } + /** + * ConnectWithOptions connects to Telnet using the supplied connection options. + */ + public ConnectWithOptions(opts: TelnetOptions): boolean { + return false; + } + /** * Info gathers information about the telnet server including encryption support. * Uses the telnetmini library's DetectEncryption helper function. @@ -90,6 +97,22 @@ export interface IsTelnetResponse { Banner?: string, } +/** + * TelnetOptions defines the connection options for a Telnet server. + */ +export interface TelnetOptions { + + Host?: string, + + Port?: number, + + Username?: string, + + Password?: string, + + Timeout?: number, +} + /** * TelnetInfoResponse is the response from the Info function. * @example diff --git a/pkg/js/generated/ts/vnc.ts b/pkg/js/generated/ts/vnc.ts index f74edb8946..56182f1e85 100644 --- a/pkg/js/generated/ts/vnc.ts +++ b/pkg/js/generated/ts/vnc.ts @@ -62,5 +62,26 @@ export class VNCClient { public Connect(host: string, port: number, password: string): boolean | null { return null; } + + /** + * ConnectWithOptions connects to VNC using the supplied connection options. + */ + public ConnectWithOptions(opts: VNCOptions): boolean | null { + return null; + } +} + +/** + * VNCOptions defines the connection options for a VNC server. + */ +export interface VNCOptions { + + Host?: string, + + Port?: number, + + Password?: string, + + Timeout?: number, } diff --git a/pkg/js/libs/ldap/ldap.go b/pkg/js/libs/ldap/ldap.go index 96761c3a70..b818e7842b 100644 --- a/pkg/js/libs/ldap/ldap.go +++ b/pkg/js/libs/ldap/ldap.go @@ -59,6 +59,14 @@ type ( ServerName string // default to host (when using tls) Upgrade bool // when true first connects to non-tls and then upgrades to tls } + + // AuthenticateOptions defines credentials for LDAP authentication. Hash takes + // precedence over Password when both are provided. + AuthenticateOptions struct { + Username string + Password string + Hash string + } ) // Constructor for creating a new ldap client @@ -161,6 +169,8 @@ func NewClient(call goja.ConstructorCall, runtime *goja.Runtime) *goja.Object { // Authenticate authenticates with the ldap server using the given username and password // performs NTLMBind first and then Bind/UnauthenticatedBind if NTLMBind fails +// +// Deprecated: prefer AuthenticateWithOptions for new templates. // @example // ```javascript // const ldap = require('nuclei/ldap'); @@ -168,24 +178,35 @@ func NewClient(call goja.ConstructorCall, runtime *goja.Runtime) *goja.Object { // client.Authenticate('user', 'password'); // ``` func (c *Client) Authenticate(username, password string) bool { + return c.AuthenticateWithOptions(AuthenticateOptions{Username: username, Password: password}) +} + +// AuthenticateWithOptions authenticates using a password or an NTLM hash. +func (c *Client) AuthenticateWithOptions(opts AuthenticateOptions) bool { c.nj.Require(c.conn != nil, "no existing connection") if c.BaseDN == "" { c.BaseDN = fmt.Sprintf("dc=%s", strings.Join(strings.Split(c.Realm, "."), ",dc=")) } - if err := c.conn.NTLMBind(c.Realm, username, password); err == nil { + if authenticationUsesHash(opts) { + if err := c.conn.NTLMBindWithHash(c.Realm, opts.Username, opts.Hash); err != nil { + c.nj.ThrowError(err) + } + return true + } + if err := c.conn.NTLMBind(c.Realm, opts.Username, opts.Password); err == nil { // if bind with NTLMBind(), there is nothing // else to do, you are authenticated return true } var err error - switch password { + switch opts.Password { case "": - if err = c.conn.UnauthenticatedBind(username); err != nil { + if err = c.conn.UnauthenticatedBind(opts.Username); err != nil { c.nj.ThrowError(err) } default: - if err = c.conn.Bind(username, password); err != nil { + if err = c.conn.Bind(opts.Username, opts.Password); err != nil { c.nj.ThrowError(err) } } @@ -200,15 +221,11 @@ func (c *Client) Authenticate(username, password string) bool { // client.AuthenticateWithNTLMHash('pdtm', 'hash'); // ``` func (c *Client) AuthenticateWithNTLMHash(username, hash string) bool { - c.nj.Require(c.conn != nil, "no existing connection") - if c.BaseDN == "" { - c.BaseDN = fmt.Sprintf("dc=%s", strings.Join(strings.Split(c.Realm, "."), ",dc=")) - } - var err error - if err = c.conn.NTLMBindWithHash(c.Realm, username, hash); err != nil { - c.nj.ThrowError(err) - } - return err == nil + return c.AuthenticateWithOptions(AuthenticateOptions{Username: username, Hash: hash}) +} + +func authenticationUsesHash(opts AuthenticateOptions) bool { + return opts.Hash != "" } // Search accepts whatever filter and returns a list of maps having provided attributes diff --git a/pkg/js/libs/ldap/ldap_test.go b/pkg/js/libs/ldap/ldap_test.go index e52325fa87..168b51a259 100644 --- a/pkg/js/libs/ldap/ldap_test.go +++ b/pkg/js/libs/ldap/ldap_test.go @@ -114,3 +114,15 @@ func requireNetworkPolicyError(t *testing.T, err error, target string) { t.Fatalf("expected network-policy denial for %q, got %q", target, err) } } + +func TestAuthenticateOptionsPreferHash(t *testing.T) { + if !authenticationUsesHash(AuthenticateOptions{Username: "user", Hash: "ntlm-hash"}) { + t.Fatal("expected hash authentication mode") + } + if !authenticationUsesHash(AuthenticateOptions{Username: "user", Password: "password", Hash: "ntlm-hash"}) { + t.Fatal("expected hash to take precedence over password") + } + if authenticationUsesHash(AuthenticateOptions{Username: "user", Password: "password"}) { + t.Fatal("expected password authentication mode") + } +} diff --git a/pkg/js/libs/mssql/mssql.go b/pkg/js/libs/mssql/mssql.go index e8cc406034..63c4c82b2a 100644 --- a/pkg/js/libs/mssql/mssql.go +++ b/pkg/js/libs/mssql/mssql.go @@ -9,7 +9,7 @@ import ( "net/url" "strings" - _ "github.com/microsoft/go-mssqldb" + mssqldb "github.com/microsoft/go-mssqldb" "github.com/projectdiscovery/nuclei/v3/pkg/js/utils" "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" ) @@ -23,9 +23,33 @@ type ( // const client = new mssql.MSSQLClient; // ``` MSSQLClient struct{} + + // MSSQLOptions defines the connection options for an MS SQL database. + // @example + // ```javascript + // const mssql = require('nuclei/mssql'); + // const opts = new mssql.MSSQLOptions(); + // opts.Host = 'acme.com'; + // opts.Port = 1433; + // opts.Username = 'sa'; + // opts.Password = 'password'; + // opts.DbName = 'master'; + // opts.Timeout = 15; + // const connected = client.ConnectWithOptions(opts); + // ``` + MSSQLOptions struct { + Host string // Host is the hostname or IP of the MS SQL server. + Port int // Port is the MS SQL port (usually 1433). + Username string // Username is the login name. + Password string // Password is the login password. + DbName string // DbName is the database name (default master when using Connect). + Timeout int // Timeout is the connection timeout in seconds (default 30). + } ) // Connect connects to MS SQL database using given credentials. +// +// Deprecated: prefer ConnectWithOptions for new templates. // If connection is successful, it returns true. // If connection is unsuccessful, it returns false and error. // The connection is closed after the function returns. @@ -41,6 +65,8 @@ func (c *MSSQLClient) Connect(ctx context.Context, host string, port int, userna } // ConnectWithDB connects to MS SQL database using given credentials and database name. +// +// Deprecated: prefer ConnectWithOptions for new templates. // If connection is successful, it returns true. // If connection is unsuccessful, it returns false and error. // The connection is closed after the function returns. @@ -55,21 +81,47 @@ func (c *MSSQLClient) ConnectWithDB(ctx context.Context, host string, port int, return memoizedconnect(ctx, executionId, host, port, username, password, dbName) } +// ConnectWithOptions connects to MS SQL using the supplied connection options. +// Prefer this over Connect/ConnectWithDB when setting timeout or database name together. +// @example +// ```javascript +// const mssql = require('nuclei/mssql'); +// const client = new mssql.MSSQLClient; +// const opts = new mssql.MSSQLOptions(); +// opts.Host = 'acme.com'; +// opts.Port = 1433; +// opts.Username = 'sa'; +// opts.Password = 'password'; +// opts.DbName = 'master'; +// opts.Timeout = 15; +// const connected = client.ConnectWithOptions(opts); +// ``` +func (c *MSSQLClient) ConnectWithOptions(ctx context.Context, opts MSSQLOptions) (bool, error) { + executionId := ctx.Value("executionId").(string) + return connectWithOptions(ctx, executionId, opts) +} + // @memo func connect(ctx context.Context, executionId string, host string, port int, username string, password string, dbName string) (bool, error) { - if host == "" || port <= 0 { + return connectWithOptions(ctx, executionId, MSSQLOptions{ + Host: host, Port: port, Username: username, Password: password, DbName: dbName, + }) +} + +func connectWithOptions(ctx context.Context, executionId string, opts MSSQLOptions) (bool, error) { + if opts.Host == "" || opts.Port <= 0 { return false, fmt.Errorf("invalid host or port") } - if !protocolstate.IsHostAllowed(executionId, host) { + if !protocolstate.IsHostAllowed(executionId, opts.Host) { // host is not valid according to network policy - return false, protocolstate.ErrHostDenied.Msgf(host) + return false, protocolstate.ErrHostDenied.Msgf(opts.Host) } - target := net.JoinHostPort(host, fmt.Sprintf("%d", port)) + target := net.JoinHostPort(opts.Host, fmt.Sprintf("%d", opts.Port)) - connString := mssqlConnString(target, username, password, dbName) + connString := mssqlConnString(target, opts) - db, err := sql.Open("sqlserver", connString) + db, err := openDB(executionId, connString) if err != nil { return false, err } @@ -156,9 +208,11 @@ func (c *MSSQLClient) ExecuteQuery(ctx context.Context, host string, port int, u } target := net.JoinHostPort(host, fmt.Sprintf("%d", port)) - connString := mssqlConnString(target, username, password, dbName) + connString := mssqlConnString(target, MSSQLOptions{ + Username: username, Password: password, DbName: dbName, + }) - db, err := sql.Open("sqlserver", connString) + db, err := openDB(executionId, connString) if err != nil { return nil, err } @@ -184,10 +238,40 @@ func (c *MSSQLClient) ExecuteQuery(ctx context.Context, host string, port int, u return data, nil } -func mssqlConnString(target, username, password, dbName string) string { - return fmt.Sprintf("sqlserver://%s:%s@%s?database=%s&connection+timeout=30", - url.PathEscape(username), - url.PathEscape(password), +func mssqlConnString(target string, opts MSSQLOptions) string { + timeout := opts.Timeout + if timeout <= 0 { + timeout = 30 + } + return fmt.Sprintf("sqlserver://%s:%s@%s?database=%s&connection+timeout=%d", + url.PathEscape(opts.Username), + url.PathEscape(opts.Password), target, - url.QueryEscape(dbName)) + url.QueryEscape(opts.DbName), + timeout) +} + +// openDB opens an MSSQL database using nuclei's policy-aware fastdialer. +func openDB(executionId, connString string) (*sql.DB, error) { + connector, err := mssqldb.NewConnector(connString) + if err != nil { + return nil, err + } + connector.Dialer = mssqlDialer{executionId: executionId} + return sql.OpenDB(connector), nil +} + +type mssqlDialer struct { + executionId string +} + +func (d mssqlDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + if !protocolstate.IsHostAllowed(d.executionId, address) { + return nil, protocolstate.ErrHostDenied.Msgf(address) + } + dialers := protocolstate.GetDialersWithId(d.executionId) + if dialers == nil { + return nil, fmt.Errorf("dialers not initialized for %s", d.executionId) + } + return dialers.Fastdialer.Dial(ctx, network, address) } diff --git a/pkg/js/libs/mssql/mssql_test.go b/pkg/js/libs/mssql/mssql_test.go index 5fb07adb06..984a91563b 100644 --- a/pkg/js/libs/mssql/mssql_test.go +++ b/pkg/js/libs/mssql/mssql_test.go @@ -1,9 +1,13 @@ package mssql import ( + "context" + "strings" "testing" "github.com/microsoft/go-mssqldb/msdsn" + "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" + "github.com/projectdiscovery/nuclei/v3/pkg/types" "github.com/stretchr/testify/require" ) @@ -14,7 +18,9 @@ func TestConnectionStringDoesNotTreatDatabaseNameAsDriverOptions(t *testing.T) { "&krb5-keytabfile=/tmp/krb5.keytab" + "&krb5-credcachefile=/tmp/krb5.ccache" - cfg, err := msdsn.Parse(mssqlConnString("127.0.0.1:1433", "user", "password", dbName)) + cfg, err := msdsn.Parse(mssqlConnString("127.0.0.1:1433", MSSQLOptions{ + Username: "user", Password: "password", DbName: dbName, + })) require.NoError(t, err) require.Equal(t, dbName, cfg.Database) @@ -27,9 +33,34 @@ func TestConnectionStringDoesNotTreatDatabaseNameAsDriverOptions(t *testing.T) { require.NotContains(t, cfg.Parameters, "krb5-credcachefile") } +func TestConnectWithOptionsValidatesTarget(t *testing.T) { + connected, err := connectWithOptions(context.Background(), "test", MSSQLOptions{Port: 1433}) + require.False(t, connected) + require.EqualError(t, err, "invalid host or port") +} + +func TestConnectWithOptionsDeniesRestrictedLocalHostBeforeDial(t *testing.T) { + executionID := t.Name() + require.NoError(t, protocolstate.Init(&types.Options{ + ExecutionId: executionID, + RestrictLocalNetworkAccess: true, + })) + t.Cleanup(func() { protocolstate.Close(executionID) }) + + connected, err := connectWithOptions(context.Background(), executionID, MSSQLOptions{ + Host: "127.0.0.1", Port: 1433, + }) + require.False(t, connected) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "network policy") && strings.Contains(err.Error(), "127.0.0.1")) +} + func TestConnectionStringKeepsPlainDatabaseName(t *testing.T) { - cfg, err := msdsn.Parse(mssqlConnString("127.0.0.1:1433", "user", "password", "master")) + cfg, err := msdsn.Parse(mssqlConnString("127.0.0.1:1433", MSSQLOptions{ + Username: "user", Password: "password", DbName: "master", Timeout: 12, + })) require.NoError(t, err) require.Equal(t, "master", cfg.Database) + require.Equal(t, "12", cfg.Parameters["connection timeout"]) } diff --git a/pkg/js/libs/mysql/mysql.go b/pkg/js/libs/mysql/mysql.go index 48f1b5b866..48f6aedd9e 100644 --- a/pkg/js/libs/mysql/mysql.go +++ b/pkg/js/libs/mysql/mysql.go @@ -48,6 +48,8 @@ func isMySQL(ctx context.Context, executionId string, host string, port int) (bo } // Connect connects to MySQL database using given credentials. +// +// Deprecated: prefer ConnectWithOptions for new templates. // If connection is successful, it returns true. // If connection is unsuccessful, it returns false and error. // The connection is closed after the function returns. @@ -58,14 +60,29 @@ func isMySQL(ctx context.Context, executionId string, host string, port int) (bo // const connected = client.Connect('acme.com', 3306, 'username', 'password'); // ``` func (c *MySQLClient) Connect(ctx context.Context, host string, port int, username, password string) (bool, error) { + return c.ConnectWithOptions(ctx, MySQLOptions{ + Host: host, + Port: port, + DbName: "INFORMATION_SCHEMA", + Protocol: "tcp", + Username: username, + Password: password, + }) +} + +// ConnectWithOptions connects to MySQL using the supplied connection options. +func (c *MySQLClient) ConnectWithOptions(ctx context.Context, opts MySQLOptions) (bool, error) { executionId := ctx.Value("executionId").(string) - if !protocolstate.IsHostAllowed(executionId, host) { + if opts.Host == "" || opts.Port <= 0 { + return false, fmt.Errorf("invalid host or port") + } + if !protocolstate.IsHostAllowed(executionId, opts.Host) { // host is not valid according to network policy - return false, protocolstate.ErrHostDenied.Msgf(host) + return false, protocolstate.ErrHostDenied.Msgf(opts.Host) } // executing queries implies the remote mysql service - ok, err := c.IsMySQL(ctx, host, port) + ok, err := c.IsMySQL(ctx, opts.Host, opts.Port) if err != nil { return false, err } @@ -73,14 +90,7 @@ func (c *MySQLClient) Connect(ctx context.Context, host string, port int, userna return false, fmt.Errorf("not a mysql service") } - dsn, err := BuildDSN(MySQLOptions{ - Host: host, - Port: port, - DbName: "INFORMATION_SCHEMA", - Protocol: "tcp", - Username: username, - Password: password, - }) + dsn, err := BuildDSN(opts) if err != nil { return false, err } diff --git a/pkg/js/libs/mysql/mysql_private.go b/pkg/js/libs/mysql/mysql_private.go index da8b453681..6b92e6a7ab 100644 --- a/pkg/js/libs/mysql/mysql_private.go +++ b/pkg/js/libs/mysql/mysql_private.go @@ -7,6 +7,7 @@ import ( "net" "net/url" "strings" + "time" "github.com/go-sql-driver/mysql" "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" @@ -51,11 +52,14 @@ func BuildDSN(opts MySQLOptions) (string, error) { if opts.Protocol == "" { opts.Protocol = "tcp" } - // We're going to use a custom dialer when creating MySQL connections, so if we've been - // given "tcp" as the protocol, then quietly switch it to "nucleitcp", which we have - // already registered. - if opts.Protocol == "tcp" { + switch opts.Protocol { + case "tcp", "nucleitcp": + // We're going to use a custom dialer when creating MySQL connections, so if we've been + // given "tcp" as the protocol, then quietly switch it to "nucleitcp", which we have + // already registered with fastdialer + network policy. opts.Protocol = "nucleitcp" + default: + return "", fmt.Errorf("unsupported mysql protocol %q (only tcp is allowed)", opts.Protocol) } if opts.DbName == "" { opts.DbName = "/" @@ -73,13 +77,22 @@ func BuildDSN(opts MySQLOptions) (string, error) { if opts.RawQuery != "" { dsn.WriteString(opts.RawQuery) } - return dsn.String(), nil + config, err := mysql.ParseDSN(dsn.String()) + if err != nil { + return "", err + } + if opts.Timeout > 0 { + config.Timeout = time.Duration(opts.Timeout) * time.Second + } + return config.FormatDSN(), nil } // sandboxDSN enforces the local file access sandbox on a MySQL DSN. The // driver's allowAllFiles option lets a malicious server read any local file // off the host via LOAD DATA LOCAL INFILE, so it is only honored when -lfa is // enabled, mirroring the fs.ReadFile restriction. +// It also rewrites the network to nucleitcp so ConnectWithDSN cannot bypass +// fastdialer / network policy via a raw tcp or unix DSN. func sandboxDSN(dsn string, lfaAllowed bool) (string, error) { cfg, err := mysql.ParseDSN(dsn) if err != nil { @@ -88,6 +101,12 @@ func sandboxDSN(dsn string, lfaAllowed bool) (string, error) { if cfg.AllowAllFiles && !lfaAllowed { cfg.AllowAllFiles = false } + switch cfg.Net { + case "", "tcp", "nucleitcp": + cfg.Net = "nucleitcp" + default: + return "", fmt.Errorf("unsupported mysql protocol %q (only tcp is allowed)", cfg.Net) + } return cfg.FormatDSN(), nil } @@ -97,6 +116,13 @@ func openDB(executionId, dsn string) (*sql.DB, error) { if err != nil { return nil, err } + cfg, err := mysql.ParseDSN(dsn) + if err != nil { + return nil, err + } + if !protocolstate.IsHostAllowed(executionId, cfg.Addr) { + return nil, protocolstate.ErrHostDenied.Msgf(cfg.Addr) + } return sql.Open("mysql", dsn) } diff --git a/pkg/js/libs/mysql/mysql_private_test.go b/pkg/js/libs/mysql/mysql_private_test.go index e48af9a7c8..41c10d9948 100644 --- a/pkg/js/libs/mysql/mysql_private_test.go +++ b/pkg/js/libs/mysql/mysql_private_test.go @@ -2,11 +2,72 @@ package mysql import ( "testing" + "time" "github.com/go-sql-driver/mysql" + "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" + "github.com/projectdiscovery/nuclei/v3/pkg/types" "github.com/stretchr/testify/require" ) +func TestBuildDSNMapsOptions(t *testing.T) { + dsn, err := BuildDSN(MySQLOptions{ + Host: "127.0.0.1", Port: 3306, Username: "user", Password: "password", + DbName: "app", Timeout: 7, RawQuery: "?tls=skip-verify", + }) + require.NoError(t, err) + + cfg, err := mysql.ParseDSN(dsn) + require.NoError(t, err) + require.Equal(t, "nucleitcp", cfg.Net) + require.Equal(t, "127.0.0.1:3306", cfg.Addr) + require.Equal(t, "app", cfg.DBName) + require.Equal(t, 7*time.Second, cfg.Timeout) + require.Equal(t, "skip-verify", cfg.TLSConfig) +} + +func TestBuildDSNRejectsMissingHostOrPort(t *testing.T) { + _, err := BuildDSN(MySQLOptions{Port: 3306}) + require.EqualError(t, err, "invalid host or port") + _, err = BuildDSN(MySQLOptions{Host: "127.0.0.1"}) + require.EqualError(t, err, "invalid host or port") +} + +func TestBuildDSNRejectsNonTCPProtocol(t *testing.T) { + _, err := BuildDSN(MySQLOptions{Host: "127.0.0.1", Port: 3306, Protocol: "unix"}) + require.Error(t, err) + require.Contains(t, err.Error(), "unsupported mysql protocol") +} + +func TestSandboxDSNForcesNucleiTCP(t *testing.T) { + got, err := sandboxDSN("root:x@tcp(127.0.0.1:3306)/", false) + require.NoError(t, err) + + cfg, err := mysql.ParseDSN(got) + require.NoError(t, err) + require.Equal(t, "nucleitcp", cfg.Net) +} + +func TestSandboxDSNRejectsUnixSocket(t *testing.T) { + _, err := sandboxDSN("root:x@unix(/var/run/mysqld.sock)/", false) + require.Error(t, err) + require.Contains(t, err.Error(), "unsupported mysql protocol") +} + +func TestOpenDBDeniesRestrictedLocalHost(t *testing.T) { + executionID := "mysql-open-deny-" + t.Name() + require.NoError(t, protocolstate.Init(&types.Options{ + ExecutionId: executionID, + RestrictLocalNetworkAccess: true, + })) + t.Cleanup(func() { protocolstate.Close(executionID) }) + + _, err := openDB(executionID, "root:x@tcp(127.0.0.1:3306)/") + require.Error(t, err) + require.Contains(t, err.Error(), "network policy") + require.Contains(t, err.Error(), "127.0.0.1") +} + func TestSandboxDSN(t *testing.T) { t.Run("strips allowAllFiles when lfa disabled", func(t *testing.T) { got, err := sandboxDSN("root:x@nucleitcp(127.0.0.1:3306)/?allowAllFiles=true", false) diff --git a/pkg/js/libs/oracle/oracle.go b/pkg/js/libs/oracle/oracle.go index cdf12fb370..34ba4fa801 100644 --- a/pkg/js/libs/oracle/oracle.go +++ b/pkg/js/libs/oracle/oracle.go @@ -39,6 +39,15 @@ type ( OracleClient struct { connector *goora.OracleConnector } + + // OracleOptions defines the connection options for an Oracle database. + OracleOptions struct { + Host string + Port int + ServiceName string + Username string + Password string + } ) // IsOracle checks if a host is running an Oracle server @@ -56,6 +65,9 @@ func (c *OracleClient) IsOracle(ctx context.Context, host string, port int) (IsO // @memo func isOracle(ctx context.Context, executionId string, host string, port int) (IsOracleResponse, error) { resp := IsOracleResponse{} + if !protocolstate.IsHostAllowed(executionId, host) { + return resp, protocolstate.ErrHostDenied.Msgf(host) + } dialer := protocolstate.GetDialersWithId(executionId) if dialer == nil { @@ -90,16 +102,25 @@ func (c *OracleClient) oracleDbInstance(ctx context.Context, connStr string, exe if err != nil { return nil, err } - - if c.connector == nil { - connector := goora.NewConnector(connStr) - oraConnector, ok := connector.(*goora.OracleConnector) - if !ok { - return nil, fmt.Errorf("failed to cast connector to OracleConnector") + config, err := goora.ParseConfig(connStr) + if err != nil { + return nil, err + } + for _, server := range config.Servers { + if !protocolstate.IsHostAllowed(executionId, server.Addr) { + return nil, protocolstate.ErrHostDenied.Msgf(server.Addr) } - c.connector = oraConnector } + connector := goora.NewConnector(connStr) + oraConnector, ok := connector.(*goora.OracleConnector) + if !ok { + return nil, fmt.Errorf("failed to cast connector to OracleConnector") + } + // A connector embeds its DSN, so replace it for each invocation to ensure + // option changes and the sandboxed DSN are applied to the actual dial. + c.connector = oraConnector + // Refresh the dialer on every call so the connector uses the current // execution context instead of a stale or already-canceled one. c.connector.Dialer(&oracleCustomDialer{executionId: executionId, ctx: ctx}) @@ -149,7 +170,9 @@ func isOracleTracePathOption(key string) bool { } } -// Connect connects to an Oracle database +// Connect connects to an Oracle database. +// +// Deprecated: prefer ConnectWithOptions for new templates. // @example // ```javascript // const oracle = require('nuclei/oracle'); @@ -157,8 +180,24 @@ func isOracleTracePathOption(key string) bool { // client.Connect('acme.com', 1521, 'XE', 'user', 'password'); // ``` func (c *OracleClient) Connect(ctx context.Context, host string, port int, serviceName string, username string, password string) (bool, error) { - connStr := goora.BuildUrl(host, port, serviceName, username, password, nil) + return c.ConnectWithOptions(ctx, OracleOptions{ + Host: host, Port: port, ServiceName: serviceName, Username: username, Password: password, + }) +} +// ConnectWithOptions connects to Oracle using the supplied connection options. +func (c *OracleClient) ConnectWithOptions(ctx context.Context, opts OracleOptions) (bool, error) { + executionId := ctx.Value("executionId").(string) + if opts.Host == "" || opts.Port <= 0 { + return false, fmt.Errorf("invalid host or port") + } + if !protocolstate.IsHostAllowed(executionId, opts.Host) { + return false, protocolstate.ErrHostDenied.Msgf(opts.Host) + } + connStr, err := buildOracleDSN(opts) + if err != nil { + return false, err + } return c.ConnectWithDSN(ctx, connStr) } @@ -187,6 +226,16 @@ func (c *OracleClient) ConnectWithDSN(ctx context.Context, dsn string) (bool, er return true, nil } +func buildOracleDSN(opts OracleOptions) (string, error) { + if opts.Host == "" || opts.Port <= 0 { + return "", fmt.Errorf("invalid host or port") + } + if opts.ServiceName == "" { + return "", fmt.Errorf("service name cannot be empty") + } + return goora.BuildUrl(opts.Host, opts.Port, opts.ServiceName, opts.Username, opts.Password, nil), nil +} + // ExecuteQuery connects to MS SQL database using given credentials and executes a query. // It returns the results of the query or an error if something goes wrong. // @example diff --git a/pkg/js/libs/oracle/oracle_test.go b/pkg/js/libs/oracle/oracle_test.go index 18ffaad03d..967e868dd5 100644 --- a/pkg/js/libs/oracle/oracle_test.go +++ b/pkg/js/libs/oracle/oracle_test.go @@ -100,3 +100,42 @@ func restoreOracleTemplatesDir(t *testing.T, templatesDir string) { config.DefaultConfig.SetTemplatesDir(oldTemplatesDir) }) } + +func TestBuildOracleDSNMapsOptions(t *testing.T) { + dsn, err := buildOracleDSN(OracleOptions{ + Host: "127.0.0.1", Port: 1521, ServiceName: "XE", Username: "user", Password: "pass", + }) + require.NoError(t, err) + + cfg, err := go_ora.ParseConfig(dsn) + require.NoError(t, err) + require.Len(t, cfg.Servers, 1) + require.Equal(t, "127.0.0.1", cfg.Servers[0].Addr) + require.Equal(t, 1521, cfg.Servers[0].Port) + require.Equal(t, "XE", cfg.ServiceName) +} + +func TestBuildOracleDSNValidatesTarget(t *testing.T) { + _, err := buildOracleDSN(OracleOptions{Port: 1521, ServiceName: "XE"}) + require.EqualError(t, err, "invalid host or port") + _, err = buildOracleDSN(OracleOptions{Host: "127.0.0.1", Port: 1521}) + require.EqualError(t, err, "service name cannot be empty") +} + +func TestConnectWithOptionsDeniesRestrictedLocalHostBeforeDial(t *testing.T) { + executionID := t.Name() + require.NoError(t, protocolstate.Init(&types.Options{ + ExecutionId: executionID, + RestrictLocalNetworkAccess: true, + })) + t.Cleanup(func() { protocolstate.Close(executionID) }) + + ctx := context.WithValue(context.Background(), "executionId", executionID) // nolint:staticcheck + connected, err := (&OracleClient{}).ConnectWithOptions(ctx, OracleOptions{ + Host: "127.0.0.1", Port: 1521, ServiceName: "XE", Username: "user", Password: "pass", + }) + require.False(t, connected) + require.Error(t, err) + require.Contains(t, err.Error(), "network policy") + require.Contains(t, err.Error(), "127.0.0.1") +} diff --git a/pkg/js/libs/postgres/postgres.go b/pkg/js/libs/postgres/postgres.go index 8edfecf0d9..3042f92055 100644 --- a/pkg/js/libs/postgres/postgres.go +++ b/pkg/js/libs/postgres/postgres.go @@ -2,6 +2,7 @@ package postgres import ( "context" + "crypto/tls" "fmt" "net" "net/url" @@ -25,6 +26,17 @@ type ( // const client = new postgres.PGClient; // ``` PGClient struct{} + + // PostgresOptions defines the connection options for a Postgres database. + PostgresOptions struct { + Host string + Port int + Username string + Password string + DbName string + Timeout int // Timeout is in seconds. + SSLMode string + } ) // IsPostgres checks if the given host and port are running Postgres database. @@ -45,6 +57,9 @@ func (c *PGClient) IsPostgres(ctx context.Context, host string, port int) (bool, func isPostgres(ctx context.Context, executionId string, host string, port int) (bool, error) { timeout := 10 * time.Second + if !protocolstate.IsHostAllowed(executionId, host) { + return false, protocolstate.ErrHostDenied.Msgf(host) + } dialer := protocolstate.GetDialersWithId(executionId) if dialer == nil { return false, fmt.Errorf("dialers not initialized for %s", executionId) @@ -72,6 +87,8 @@ func isPostgres(ctx context.Context, executionId string, host string, port int) } // Connect connects to Postgres database using given credentials. +// +// Deprecated: prefer ConnectWithOptions for new templates. // If connection is successful, it returns true. // If connection is unsuccessful, it returns false and error. // The connection is closed after the function returns. @@ -93,6 +110,25 @@ func (c *PGClient) Connect(ctx context.Context, host string, port int, username return memoizedconnect(ctx, executionId, host, port, username, password, "postgres") } +// ConnectWithOptions connects to Postgres using the supplied connection options. +func (c *PGClient) ConnectWithOptions(ctx context.Context, opts PostgresOptions) (bool, error) { + if opts.Host == "" || opts.Port <= 0 { + return false, fmt.Errorf("invalid host or port") + } + if _, err := postgresTLSConfig(opts.SSLMode, opts.Host); err != nil { + return false, err + } + ok, err := c.IsPostgres(ctx, opts.Host, opts.Port) + if err != nil { + return false, err + } + if !ok { + return false, fmt.Errorf("not a postgres service") + } + executionId := ctx.Value("executionId").(string) + return connectWithOptions(ctx, executionId, opts) +} + // ExecuteQuery connects to Postgres database using given credentials and database name. // and executes a query on the db. // If connection is successful, it returns the result of the query. @@ -147,19 +183,31 @@ func executeQuery(ctx context.Context, executionId string, host string, port int } func buildPostgresConnURL(username, password, target, dbName, executionId string) string { + return buildPostgresConnURLWithOptions(PostgresOptions{ + Username: username, Password: password, DbName: dbName, + }, target, executionId) +} + +func buildPostgresConnURLWithOptions(opts PostgresOptions, target, executionId string) string { values := url.Values{} - values.Set("sslmode", "disable") + sslMode := opts.SSLMode + if sslMode == "" { + sslMode = "disable" + } + values.Set("sslmode", sslMode) values.Set("executionId", executionId) return fmt.Sprintf("postgres://%s@%s/%s?%s", - url.UserPassword(username, password).String(), + url.UserPassword(opts.Username, opts.Password).String(), target, - url.PathEscape(dbName), + url.PathEscape(opts.DbName), values.Encode(), ) } // ConnectWithDB connects to Postgres database using given credentials and database name. +// +// Deprecated: prefer ConnectWithOptions for new templates. // If connection is successful, it returns true. // If connection is unsuccessful, it returns false and error. // The connection is closed after the function returns. @@ -185,16 +233,26 @@ func (c *PGClient) ConnectWithDB(ctx context.Context, host string, port int, use // @memo func connect(ctx context.Context, executionId string, host string, port int, username string, password string, dbName string) (bool, error) { - if host == "" || port <= 0 { + return connectWithOptions(ctx, executionId, PostgresOptions{ + Host: host, Port: port, Username: username, Password: password, DbName: dbName, + }) +} + +func connectWithOptions(ctx context.Context, executionId string, opts PostgresOptions) (bool, error) { + if opts.Host == "" || opts.Port <= 0 { return false, fmt.Errorf("invalid host or port") } - if !protocolstate.IsHostAllowed(executionId, host) { + if !protocolstate.IsHostAllowed(executionId, opts.Host) { // host is not valid according to network policy - return false, protocolstate.ErrHostDenied.Msgf(host) + return false, protocolstate.ErrHostDenied.Msgf(opts.Host) } - target := net.JoinHostPort(host, fmt.Sprintf("%d", port)) + target := net.JoinHostPort(opts.Host, fmt.Sprintf("%d", opts.Port)) + tlsConfig, err := postgresTLSConfig(opts.SSLMode, opts.Host) + if err != nil { + return false, err + } execCtx, cancel := context.WithCancel(ctx) defer cancel() @@ -205,21 +263,25 @@ func connect(ctx context.Context, executionId string, host string, port int, use } db := pg.Connect(&pg.Options{ - Addr: target, - User: username, - Password: password, - Database: dbName, + Addr: target, + User: opts.Username, + Password: opts.Password, + Database: opts.DbName, + TLSConfig: tlsConfig, + DialTimeout: postgresTimeout(opts.Timeout), + ReadTimeout: postgresTimeout(opts.Timeout), + WriteTimeout: postgresTimeout(opts.Timeout), Dialer: func(dialCtx context.Context, network, addr string) (net.Conn, error) { return dialer.Fastdialer.Dial(dialCtx, network, addr) }, IdleCheckFrequency: -1, - }).WithTimeout(10 * time.Second) + }).WithTimeout(postgresTimeout(opts.Timeout)) defer func() { _ = db.Close() }() - _, err := db.ExecContext(execCtx, "select 1") + _, err = db.ExecContext(execCtx, "select 1") if err != nil { switch true { case strings.Contains(err.Error(), "connect: connection refused"): @@ -237,3 +299,32 @@ func connect(ctx context.Context, executionId string, host string, port int, use } return true, nil } + +func postgresTimeout(timeout int) time.Duration { + if timeout > 0 { + return time.Duration(timeout) * time.Second + } + return 10 * time.Second +} + +func postgresTLSConfig(sslMode, host string) (*tls.Config, error) { + switch sslMode { + case "", "disable": + return nil, nil + case "allow", "prefer", "require": + // libpq semantics: encrypt the session without verifying the server cert. + return &tls.Config{ + InsecureSkipVerify: true, //nolint:gosec // intentional sslmode=require/prefer/allow + MinVersion: tls.VersionTLS12, + }, nil + case "verify-ca", "verify-full": + // go-pg does not set ServerName from Addr; set it explicitly so + // hostname verification works for verify-full (and is harmless for verify-ca). + return &tls.Config{ + ServerName: host, + MinVersion: tls.VersionTLS12, + }, nil + default: + return nil, fmt.Errorf("unsupported postgres sslmode %q", sslMode) + } +} diff --git a/pkg/js/libs/postgres/postgres_test.go b/pkg/js/libs/postgres/postgres_test.go index 71397521ef..8c73b6dcd9 100644 --- a/pkg/js/libs/postgres/postgres_test.go +++ b/pkg/js/libs/postgres/postgres_test.go @@ -1,11 +1,14 @@ package postgres import ( + "context" "net/url" "strings" "testing" "github.com/lib/pq" + "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" + "github.com/projectdiscovery/nuclei/v3/pkg/types" ) func TestBuildPostgresConnectionURLDoesNotAllowDBNameQueryInjection(t *testing.T) { @@ -72,3 +75,77 @@ func TestBuildPostgresConnectionURLEscapesCredentials(t *testing.T) { t.Fatalf("database name = %q, want postgres", got) } } + +func TestBuildPostgresConnectionURLMapsOptions(t *testing.T) { + connStr := buildPostgresConnURLWithOptions(PostgresOptions{ + Username: "user", Password: "password", DbName: "app", SSLMode: "require", + }, "127.0.0.1:5432", "exec-1") + + u, err := url.Parse(connStr) + if err != nil { + t.Fatalf("parse connection URL: %v", err) + } + if got := u.Query().Get("sslmode"); got != "require" { + t.Fatalf("sslmode = %q, want require", got) + } + if got := strings.TrimPrefix(u.Path, "/"); got != "app" { + t.Fatalf("database name = %q, want app", got) + } +} + +func TestPostgresTimeout(t *testing.T) { + if got := postgresTimeout(3); got.String() != "3s" { + t.Fatalf("timeout = %s, want 3s", got) + } + if got := postgresTimeout(0); got.String() != "10s" { + t.Fatalf("default timeout = %s, want 10s", got) + } +} + +func TestPostgresTLSConfigMapsSSLMode(t *testing.T) { + tlsConfig, err := postgresTLSConfig("require", "db.example.com") + if err != nil { + t.Fatalf("map require SSL mode: %v", err) + } + if tlsConfig == nil || !tlsConfig.InsecureSkipVerify { + t.Fatal("require SSL mode should enable TLS without certificate verification") + } + + tlsConfig, err = postgresTLSConfig("disable", "db.example.com") + if err != nil { + t.Fatalf("map disable SSL mode: %v", err) + } + if tlsConfig != nil { + t.Fatal("disable SSL mode should not configure TLS") + } + if _, err := postgresTLSConfig("invalid", "db.example.com"); err == nil { + t.Fatal("expected unsupported SSL mode error") + } + + tlsConfig, err = postgresTLSConfig("verify-full", "db.example.com") + if err != nil { + t.Fatalf("map verify-full SSL mode: %v", err) + } + if tlsConfig == nil || tlsConfig.ServerName != "db.example.com" || tlsConfig.InsecureSkipVerify { + t.Fatalf("verify-full should set ServerName and verify certs, got %+v", tlsConfig) + } +} + +func TestConnectWithOptionsDeniesRestrictedLocalHostBeforeDial(t *testing.T) { + executionID := t.Name() + if err := protocolstate.Init(&types.Options{ + ExecutionId: executionID, + RestrictLocalNetworkAccess: true, + }); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { protocolstate.Close(executionID) }) + + ctx := context.WithValue(context.Background(), "executionId", executionID) // nolint:staticcheck + connected, err := (&PGClient{}).ConnectWithOptions(ctx, PostgresOptions{ + Host: "127.0.0.1", Port: 5432, SSLMode: "require", + }) + if connected || err == nil || !strings.Contains(err.Error(), "network policy") { + t.Fatalf("expected network-policy denial before dialing, got connected=%t err=%v", connected, err) + } +} diff --git a/pkg/js/libs/redis/redis.go b/pkg/js/libs/redis/redis.go index 16820e8f18..a60f2b83c4 100644 --- a/pkg/js/libs/redis/redis.go +++ b/pkg/js/libs/redis/redis.go @@ -3,6 +3,7 @@ package redis import ( "context" "fmt" + "net" "time" "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" @@ -12,6 +13,26 @@ import ( pluginsredis "github.com/praetorian-inc/fingerprintx/pkg/plugins/services/redis" ) +// RedisOptions defines the connection options for a Redis server. +// @example +// ```javascript +// const redis = require('nuclei/redis'); +// const opts = new redis.RedisOptions(); +// opts.Host = 'acme.com'; +// opts.Port = 6379; +// opts.Password = 'password'; +// opts.DB = 0; +// opts.Timeout = 10; +// const connected = redis.ConnectWithOptions(opts); +// ``` +type RedisOptions struct { + Host string // Host is the hostname or IP of the Redis server. + Port int // Port is the Redis port (usually 6379). + Password string // Password is the Redis AUTH password. + DB int // DB is the Redis database index. + Timeout int // Timeout is the dial/read/write timeout in seconds. +} + // GetServerInfo returns the server info for a redis server // @example // ```javascript @@ -30,11 +51,7 @@ func getServerInfo(ctx context.Context, executionId string, host string, port in return "", protocolstate.ErrHostDenied.Msgf(host) } // create a new client - client := redis.NewClient(&redis.Options{ - Addr: fmt.Sprintf("%s:%d", host, port), - Password: "", // no password set - DB: 0, // use default DB - }) + client := redis.NewClient(redisClientOptions(executionId, RedisOptions{Host: host, Port: port})) defer func() { _ = client.Close() }() @@ -54,7 +71,9 @@ func getServerInfo(ctx context.Context, executionId string, host string, port in return infoCmd.Val(), nil } -// Connect tries to connect redis server with password +// Connect tries to connect redis server with password. +// +// Deprecated: prefer ConnectWithOptions for new templates. // @example // ```javascript // const redis = require('nuclei/redis'); @@ -65,18 +84,37 @@ func Connect(ctx context.Context, host string, port int, password string) (bool, return memoizedconnect(ctx, executionId, host, port, password) } +// ConnectWithOptions tries to connect to Redis using the supplied options. +// @example +// ```javascript +// const redis = require('nuclei/redis'); +// const opts = new redis.RedisOptions(); +// opts.Host = 'acme.com'; +// opts.Port = 6379; +// opts.Password = 'password'; +// opts.DB = 1; +// const connected = redis.ConnectWithOptions(opts); +// ``` +func ConnectWithOptions(ctx context.Context, opts RedisOptions) (bool, error) { + executionId := ctx.Value("executionId").(string) + return connectWithOptions(ctx, executionId, opts) +} + // @memo func connect(ctx context.Context, executionId string, host string, port int, password string) (bool, error) { - if !protocolstate.IsHostAllowed(executionId, host) { + return connectWithOptions(ctx, executionId, RedisOptions{Host: host, Port: port, Password: password}) +} + +func connectWithOptions(ctx context.Context, executionId string, opts RedisOptions) (bool, error) { + if opts.Host == "" || opts.Port <= 0 { + return false, fmt.Errorf("invalid host or port") + } + if !protocolstate.IsHostAllowed(executionId, opts.Host) { // host is not valid according to network policy - return false, protocolstate.ErrHostDenied.Msgf(host) + return false, protocolstate.ErrHostDenied.Msgf(opts.Host) } // create a new client - client := redis.NewClient(&redis.Options{ - Addr: fmt.Sprintf("%s:%d", host, port), - Password: password, // no password set - DB: 0, // use default DB - }) + client := redis.NewClient(redisClientOptions(executionId, opts)) defer func() { _ = client.Close() }() @@ -94,6 +132,35 @@ func connect(ctx context.Context, executionId string, host string, port int, pas return true, nil } +func redisClientOptions(executionId string, opts RedisOptions) *redis.Options { + clientOpts := &redis.Options{ + Addr: fmt.Sprintf("%s:%d", opts.Host, opts.Port), + Password: opts.Password, + DB: opts.DB, + Dialer: redisDialer(executionId), + } + if opts.Timeout > 0 { + timeout := time.Duration(opts.Timeout) * time.Second + clientOpts.DialTimeout = timeout + clientOpts.ReadTimeout = timeout + clientOpts.WriteTimeout = timeout + } + return clientOpts +} + +func redisDialer(executionId string) func(context.Context, string, string) (net.Conn, error) { + return func(ctx context.Context, network, address string) (net.Conn, error) { + if !protocolstate.IsHostAllowed(executionId, address) { + return nil, protocolstate.ErrHostDenied.Msgf(address) + } + dialers := protocolstate.GetDialersWithId(executionId) + if dialers == nil { + return nil, fmt.Errorf("dialers not initialized for %s", executionId) + } + return dialers.Fastdialer.Dial(ctx, network, address) + } +} + // GetServerInfoAuth returns the server info for a redis server // @example // ```javascript @@ -112,11 +179,7 @@ func getServerInfoAuth(ctx context.Context, executionId string, host string, por return "", protocolstate.ErrHostDenied.Msgf(host) } // create a new client - client := redis.NewClient(&redis.Options{ - Addr: fmt.Sprintf("%s:%d", host, port), - Password: password, // no password set - DB: 0, // use default DB - }) + client := redis.NewClient(redisClientOptions(executionId, RedisOptions{Host: host, Port: port, Password: password})) defer func() { _ = client.Close() }() @@ -149,6 +212,9 @@ func IsAuthenticated(ctx context.Context, host string, port int) (bool, error) { // @memo func isAuthenticated(ctx context.Context, executionId string, host string, port int) (bool, error) { + if !protocolstate.IsHostAllowed(executionId, host) { + return false, protocolstate.ErrHostDenied.Msgf(host) + } plugin := pluginsredis.REDISPlugin{} timeout := 5 * time.Second dialer := protocolstate.GetDialersWithId(executionId) @@ -187,11 +253,7 @@ func RunLuaScript(ctx context.Context, host string, port int, password string, s return false, protocolstate.ErrHostDenied.Msgf(host) } // create a new client - client := redis.NewClient(&redis.Options{ - Addr: fmt.Sprintf("%s:%d", host, port), - Password: password, - DB: 0, // use default DB - }) + client := redis.NewClient(redisClientOptions(executionId, RedisOptions{Host: host, Port: port, Password: password})) defer func() { _ = client.Close() }() diff --git a/pkg/js/libs/redis/redis_options_test.go b/pkg/js/libs/redis/redis_options_test.go new file mode 100644 index 0000000000..d709b91e29 --- /dev/null +++ b/pkg/js/libs/redis/redis_options_test.go @@ -0,0 +1,49 @@ +package redis + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" + "github.com/projectdiscovery/nuclei/v3/pkg/types" +) + +func TestRedisClientOptionsMapsConnectionOptions(t *testing.T) { + opts := redisClientOptions("test", RedisOptions{ + Host: "127.0.0.1", Port: 6379, Password: "secret", DB: 3, Timeout: 4, + }) + + if opts.Addr != "127.0.0.1:6379" || opts.Password != "secret" || opts.DB != 3 { + t.Fatalf("unexpected Redis client options: %#v", opts) + } + if opts.DialTimeout != 4*time.Second || opts.ReadTimeout != 4*time.Second || opts.WriteTimeout != 4*time.Second { + t.Fatalf("timeout mapping failed: %#v", opts) + } +} + +func TestConnectWithOptionsValidatesTarget(t *testing.T) { + connected, err := connectWithOptions(context.Background(), "test", RedisOptions{Port: 6379}) + if connected || err == nil || err.Error() != "invalid host or port" { + t.Fatalf("expected invalid target error, got connected=%t err=%v", connected, err) + } +} + +func TestConnectWithOptionsDeniesRestrictedLocalHostBeforeDial(t *testing.T) { + executionID := t.Name() + if err := protocolstate.Init(&types.Options{ + ExecutionId: executionID, + RestrictLocalNetworkAccess: true, + }); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { protocolstate.Close(executionID) }) + + connected, err := connectWithOptions(context.Background(), executionID, RedisOptions{ + Host: "127.0.0.1", Port: 6379, Timeout: 1, + }) + if connected || err == nil || !strings.Contains(err.Error(), "network policy") { + t.Fatalf("expected network-policy denial before dialing, got connected=%t err=%v", connected, err) + } +} diff --git a/pkg/js/libs/smb/options_test.go b/pkg/js/libs/smb/options_test.go new file mode 100644 index 0000000000..98f03effe8 --- /dev/null +++ b/pkg/js/libs/smb/options_test.go @@ -0,0 +1,51 @@ +package smb + +import ( + "context" + "strings" + "testing" + + "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" + "github.com/projectdiscovery/nuclei/v3/pkg/types" + "github.com/stretchr/testify/require" +) + +func TestListSharesWithOptionsRejectsInvalidHostPort(t *testing.T) { + _, err := listSharesWithOptions(t.Context(), "exec", SMBOptions{Host: "", Port: 445}) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid host or port") + + _, err = listSharesWithOptions(t.Context(), "exec", SMBOptions{Host: "acme.com", Port: 0}) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid host or port") +} + +func TestListSharesWithOptionsDeniesRestrictedLocalHostBeforeDial(t *testing.T) { + executionID := t.Name() + require.NoError(t, protocolstate.Init(&types.Options{ + ExecutionId: executionID, + RestrictLocalNetworkAccess: true, + })) + t.Cleanup(func() { protocolstate.Close(executionID) }) + + _, err := listSharesWithOptions(context.Background(), executionID, SMBOptions{ + Host: "127.0.0.1", Port: 445, Domain: "ACME", Timeout: 1, + }) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "network policy") && strings.Contains(err.Error(), "127.0.0.1")) +} + +func TestSMBOptionsFields(t *testing.T) { + opts := SMBOptions{ + Host: "dc.acme.com", + Port: 445, + User: "Administrator", + Password: "secret", + Hash: "31d6cfe0d16ae931b73c59d7e0c089c0", + Domain: "ACME", + Timeout: 15, + } + require.Equal(t, "ACME", opts.Domain) + require.Equal(t, 15, opts.Timeout) + require.Equal(t, "31d6cfe0d16ae931b73c59d7e0c089c0", opts.Hash) +} diff --git a/pkg/js/libs/smb/smb.go b/pkg/js/libs/smb/smb.go index ac85a20f79..4899e3feee 100644 --- a/pkg/js/libs/smb/smb.go +++ b/pkg/js/libs/smb/smb.go @@ -150,6 +150,8 @@ func (c *SMBClient) ListSMBv2Metadata(ctx context.Context, host string, port int // and list shares by using given credentials. // Credentials cannot be blank. guest or anonymous credentials // can be used by providing empty password. +// +// Deprecated: prefer ListSharesWithOptions for new templates. // @example // ```javascript // const smb = require('nuclei/smb'); @@ -166,6 +168,50 @@ func (c *SMBClient) ListShares(ctx context.Context, host string, port int, user, return memoizedlistShares(ctx, executionId, host, port, user, password) } +type ( + // SMBOptions represents configuration for authenticated SMB operations. + // @example + // ```javascript + // const smb = require('nuclei/smb'); + // const client = new smb.SMBClient(); + // const opts = new smb.SMBOptions(); + // opts.Host = 'acme.com'; + // opts.Port = 445; + // opts.User = 'Administrator'; + // opts.Password = 'password'; + // opts.Domain = 'ACME'; + // const shares = client.ListSharesWithOptions(opts); + // ``` + SMBOptions struct { + Host string // Host is the hostname or IP of the SMB server. + Port int // Port is the SMB port (usually 445). + User string // User is the username for NTLM authentication. + Password string // Password is the password for NTLM authentication. + Hash string // Hash is an optional hex-encoded NTLM hash (pass-the-hash). + Domain string // Domain is the NTLM domain / workgroup. + Timeout int // Timeout is the dial timeout in seconds (default 10). + } +) + +// ListSharesWithOptions lists SMB share names using SMBOptions. +// Supports domain and pass-the-hash via smbsession. +// @example +// ```javascript +// const smb = require('nuclei/smb'); +// const client = new smb.SMBClient(); +// const opts = new smb.SMBOptions(); +// opts.Host = 'acme.com'; +// opts.Port = 445; +// opts.User = 'Administrator'; +// opts.Hash = 'aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0'; +// opts.Domain = 'ACME'; +// const shares = client.ListSharesWithOptions(opts); +// ``` +func (c *SMBClient) ListSharesWithOptions(ctx context.Context, opts SMBOptions) ([]string, error) { + executionId := ctx.Value("executionId").(string) + return listSharesWithOptions(ctx, executionId, opts) +} + // ListDir lists files and directories under path on the given share // (nmap smb-ls). path may be empty or "." for the share root. // user may be "DOMAIN\\user" or "user@domain". diff --git a/pkg/js/libs/smb/smb_share.go b/pkg/js/libs/smb/smb_share.go index 7beed80cc1..13708976c9 100644 --- a/pkg/js/libs/smb/smb_share.go +++ b/pkg/js/libs/smb/smb_share.go @@ -2,6 +2,9 @@ package smb import ( "context" + "fmt" + "strings" + "time" "github.com/projectdiscovery/nuclei/v3/pkg/js/libs/smbsession" ) @@ -11,7 +14,41 @@ type ShareEntry = smbsession.Entry // @memo func listShares(ctx context.Context, executionId string, host string, port int, user string, password string) ([]string, error) { - sess, err := smbsession.Dial(ctx, executionId, host, port, smbsession.Creds{User: user, Password: password}) + return listSharesWithOptions(ctx, executionId, SMBOptions{ + Host: host, + Port: port, + User: user, + Password: password, + }) +} + +func listSharesWithOptions(ctx context.Context, executionId string, opts SMBOptions) ([]string, error) { + if opts.Host == "" || opts.Port <= 0 { + return nil, fmt.Errorf("invalid host or port") + } + + timeout := 10 * time.Second + if opts.Timeout > 0 { + timeout = time.Duration(opts.Timeout) * time.Second + } + dialCtx := ctx + if _, hasDeadline := ctx.Deadline(); !hasDeadline { + var cancel context.CancelFunc + dialCtx, cancel = context.WithTimeout(ctx, timeout) + defer cancel() + } + + hash := opts.Hash + if i := strings.LastIndex(hash, ":"); i >= 0 { + hash = hash[i+1:] + } + + sess, err := smbsession.Dial(dialCtx, executionId, opts.Host, opts.Port, smbsession.Creds{ + User: opts.User, + Password: opts.Password, + Domain: opts.Domain, + Hash: hash, + }) if err != nil { return nil, err } diff --git a/pkg/js/libs/ssh/memo.ssh.go b/pkg/js/libs/ssh/memo.ssh.go index 0bfed5849e..929cc91ee1 100644 --- a/pkg/js/libs/ssh/memo.ssh.go +++ b/pkg/js/libs/ssh/memo.ssh.go @@ -2,6 +2,7 @@ package ssh import ( + "context" "errors" "fmt" @@ -10,11 +11,13 @@ import ( "github.com/zmap/zgrab2/lib/ssh" ) -func memoizedconnectSSHInfoMode(opts *connectOptions) (*ssh.HandshakeLog, error) { +func memoizedconnectSSHInfoMode(ctx context.Context, opts *connectOptions) (*ssh.HandshakeLog, error) { hash := "connectSSHInfoMode" + ":" + fmt.Sprint(opts) v, err, _ := protocolstate.Memoizer.Do(hash, func() (interface{}, error) { - return connectSSHInfoMode(opts) + // Shared flight must not use a caller-scoped ctx: concurrent callers + // dedupe on hash, and one cancel would abort everyone's handshake. + return connectSSHInfoMode(context.WithoutCancel(ctx), opts) }) if err != nil { return nil, err diff --git a/pkg/js/libs/ssh/options_test.go b/pkg/js/libs/ssh/options_test.go new file mode 100644 index 0000000000..5062ead981 --- /dev/null +++ b/pkg/js/libs/ssh/options_test.go @@ -0,0 +1,84 @@ +package ssh + +import ( + "strings" + "testing" + "time" + + "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" + "github.com/projectdiscovery/nuclei/v3/pkg/types" + "github.com/stretchr/testify/require" +) + +func TestOptionsToConnectMapsFields(t *testing.T) { + opts := SSHOptions{ + Host: "ssh.example.com", + Port: 2222, + User: "alice", + Password: "secret", + PrivateKey: "-----BEGIN PRIVATE KEY-----\n...", + Timeout: 15, + ClientVersion: "SSH-2.0-OpenSSH_8.9", + } + + got := optionsToConnect(opts, "exec-1") + require.Equal(t, "ssh.example.com", got.Host) + require.Equal(t, 2222, got.Port) + require.Equal(t, "alice", got.User) + require.Equal(t, "secret", got.Password) + require.Equal(t, "-----BEGIN PRIVATE KEY-----\n...", got.PrivateKey) + require.Equal(t, 15*time.Second, got.Timeout) + require.Equal(t, "SSH-2.0-OpenSSH_8.9", got.ClientVersion) + require.Equal(t, "exec-1", got.ExecutionId) +} + +func TestOptionsToConnectLeavesDefaultTimeoutWhenUnset(t *testing.T) { + got := optionsToConnect(SSHOptions{Host: "h", Port: 22}, "exec") + require.Equal(t, time.Duration(0), got.Timeout) +} + +func TestConnectOptionsValidate(t *testing.T) { + t.Run("missing host", func(t *testing.T) { + err := (&connectOptions{Port: 22, ExecutionId: "x"}).validate() + require.Error(t, err) + require.Contains(t, err.Error(), "host is required") + }) + t.Run("missing port", func(t *testing.T) { + err := (&connectOptions{Host: "h", ExecutionId: "x"}).validate() + require.Error(t, err) + require.Contains(t, err.Error(), "port is required") + }) + t.Run("default timeout", func(t *testing.T) { + // Unknown execution id → IsHostAllowed returns true (no dialers), + // so validate reaches the timeout default without network-policy noise. + opts := &connectOptions{Host: "scan.example", Port: 22, Timeout: 0, ExecutionId: "ssh-options-timeout-test"} + require.NoError(t, opts.validate()) + require.Equal(t, 10*time.Second, opts.Timeout) + }) +} + +func TestSSHOptionsPasswordAndKeyCanBothBeSet(t *testing.T) { + opts := SSHOptions{ + Host: "h", + Port: 22, + User: "u", + Password: "p", + PrivateKey: "k", + } + got := optionsToConnect(opts, "e") + require.Equal(t, "p", got.Password) + require.Equal(t, "k", got.PrivateKey) +} + +func TestConnectOptionsDeniesRestrictedLocalHostBeforeDial(t *testing.T) { + executionID := t.Name() + require.NoError(t, protocolstate.Init(&types.Options{ + ExecutionId: executionID, + RestrictLocalNetworkAccess: true, + })) + t.Cleanup(func() { protocolstate.Close(executionID) }) + + err := optionsToConnect(SSHOptions{Host: "127.0.0.1", Port: 22}, executionID).validate() + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "network policy") && strings.Contains(err.Error(), "127.0.0.1")) +} diff --git a/pkg/js/libs/ssh/ssh.go b/pkg/js/libs/ssh/ssh.go index 4a1b8d93e1..9af78d0d8b 100644 --- a/pkg/js/libs/ssh/ssh.go +++ b/pkg/js/libs/ssh/ssh.go @@ -45,10 +45,84 @@ func (c *SSHClient) SetTimeout(sec int) { c.timeout = time.Duration(sec) * time.Second } +type ( + // SSHOptions represents configuration options for an SSH connection. + // Use ConnectWithOptions when you need protocol-specific knobs beyond + // username/password (timeout, client version, private key). + // @example + // ```javascript + // const ssh = require('nuclei/ssh'); + // const client = new ssh.SSHClient(); + // const opts = new ssh.SSHOptions(); + // opts.Host = 'acme.com'; + // opts.Port = 22; + // opts.User = 'username'; + // opts.Password = 'password'; + // opts.Timeout = 15; + // opts.ClientVersion = 'SSH-2.0-OpenSSH_8.9'; + // const connected = client.ConnectWithOptions(opts); + // ``` + SSHOptions struct { + Host string // Host is the hostname or IP of the SSH server. + Port int // Port is the port number of the SSH server. + User string // User is the username for authentication. + Password string // Password is the password for authentication. + PrivateKey string // PrivateKey is an optional PEM-encoded private key. + Timeout int // Timeout is the connection timeout in seconds (default 10). + ClientVersion string // ClientVersion overrides the SSH client version string (e.g. "SSH-2.0-OpenSSH_8.9"). + } +) + +// ConnectWithOptions tries to connect using the provided SSHOptions. +// Prefer this over Connect/ConnectWithKey when setting timeout, client version, +// or combining password and private key auth. +// @example +// ```javascript +// const ssh = require('nuclei/ssh'); +// const client = new ssh.SSHClient(); +// const opts = new ssh.SSHOptions(); +// opts.Host = 'acme.com'; +// opts.Port = 22; +// opts.User = 'username'; +// opts.Password = 'password'; +// const connected = client.ConnectWithOptions(opts); +// ``` +func (c *SSHClient) ConnectWithOptions(ctx context.Context, opts SSHOptions) (bool, error) { + executionId := ctx.Value("executionId").(string) + copts := optionsToConnect(opts, executionId) + if c.timeout > 0 && copts.Timeout == 0 { + copts.Timeout = c.timeout + } + conn, err := connect(ctx, copts) + if err != nil { + return false, err + } + c.connection = conn + return true, nil +} + +func optionsToConnect(opts SSHOptions, executionId string) *connectOptions { + copts := &connectOptions{ + Host: opts.Host, + Port: opts.Port, + User: opts.User, + Password: opts.Password, + PrivateKey: opts.PrivateKey, + ClientVersion: opts.ClientVersion, + ExecutionId: executionId, + } + if opts.Timeout > 0 { + copts.Timeout = time.Duration(opts.Timeout) * time.Second + } + return copts +} + // Connect tries to connect to provided host and port // with provided username and password with ssh. // Returns state of connection and error. If error is not nil, // state will be false +// +// Deprecated: prefer ConnectWithOptions for new templates. // @example // ```javascript // const ssh = require('nuclei/ssh'); @@ -56,26 +130,20 @@ func (c *SSHClient) SetTimeout(sec int) { // const connected = client.Connect('acme.com', 22, 'username', 'password'); // ``` func (c *SSHClient) Connect(ctx context.Context, host string, port int, username, password string) (bool, error) { - executionId := ctx.Value("executionId").(string) - conn, err := connect(&connectOptions{ - Host: host, - Port: port, - User: username, - Password: password, - ExecutionId: executionId, + return c.ConnectWithOptions(ctx, SSHOptions{ + Host: host, + Port: port, + User: username, + Password: password, }) - if err != nil { - return false, err - } - c.connection = conn - - return true, nil } // ConnectWithKey tries to connect to provided host and port // with provided username and private_key. // Returns state of connection and error. If error is not nil, // state will be false +// +// Deprecated: prefer ConnectWithOptions (set PrivateKey) for new templates. // @example // ```javascript // const ssh = require('nuclei/ssh'); @@ -84,21 +152,12 @@ func (c *SSHClient) Connect(ctx context.Context, host string, port int, username // const connected = client.ConnectWithKey('acme.com', 22, 'username', privateKey); // ``` func (c *SSHClient) ConnectWithKey(ctx context.Context, host string, port int, username, key string) (bool, error) { - executionId := ctx.Value("executionId").(string) - conn, err := connect(&connectOptions{ - Host: host, - Port: port, - User: username, - PrivateKey: key, - ExecutionId: executionId, + return c.ConnectWithOptions(ctx, SSHOptions{ + Host: host, + Port: port, + User: username, + PrivateKey: key, }) - - if err != nil { - return false, err - } - c.connection = conn - - return true, nil } // ConnectSSHInfoMode tries to connect to provided host and port @@ -116,7 +175,7 @@ func (c *SSHClient) ConnectWithKey(ctx context.Context, host string, port int, u // ``` func (c *SSHClient) ConnectSSHInfoMode(ctx context.Context, host string, port int) (*ssh.HandshakeLog, error) { executionId := ctx.Value("executionId").(string) - return memoizedconnectSSHInfoMode(&connectOptions{ + return memoizedconnectSSHInfoMode(ctx, &connectOptions{ Host: host, Port: port, ExecutionId: executionId, @@ -175,13 +234,14 @@ func (c *SSHClient) Close() (bool, error) { // unexported functions type connectOptions struct { - Host string - Port int - User string - Password string - PrivateKey string - Timeout time.Duration // default 10s - ExecutionId string + Host string + Port int + User string + Password string + PrivateKey string + Timeout time.Duration // default 10s + ClientVersion string + ExecutionId string } func (c *connectOptions) validate() error { @@ -202,7 +262,7 @@ func (c *connectOptions) validate() error { } // @memo -func connectSSHInfoMode(opts *connectOptions) (*ssh.HandshakeLog, error) { +func connectSSHInfoMode(ctx context.Context, opts *connectOptions) (*ssh.HandshakeLog, error) { if err := opts.validate(); err != nil { return nil, err } @@ -218,7 +278,7 @@ func connectSSHInfoMode(opts *connectOptions) (*ssh.HandshakeLog, error) { return nil } rhost := fmt.Sprintf("%s:%d", opts.Host, opts.Port) - client, err := ssh.Dial("tcp", rhost, sshConfig) + client, err := dialSSH(ctx, opts.ExecutionId, rhost, sshConfig) if err != nil { return nil, err } @@ -229,15 +289,16 @@ func connectSSHInfoMode(opts *connectOptions) (*ssh.HandshakeLog, error) { return data, nil } -func connect(opts *connectOptions) (*ssh.Client, error) { +func connect(ctx context.Context, opts *connectOptions) (*ssh.Client, error) { if err := opts.validate(); err != nil { return nil, err } conf := &ssh.ClientConfig{ - User: opts.User, - Auth: []ssh.AuthMethod{}, - Timeout: opts.Timeout, + User: opts.User, + Auth: []ssh.AuthMethod{}, + Timeout: opts.Timeout, + ClientVersion: opts.ClientVersion, } if len(opts.Password) > 0 { @@ -271,9 +332,36 @@ func connect(opts *connectOptions) (*ssh.Client, error) { conf.Auth = append(conf.Auth, ssh.PublicKeys(signer)) } - client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", opts.Host, opts.Port), conf) + client, err := dialSSH(ctx, opts.ExecutionId, fmt.Sprintf("%s:%d", opts.Host, opts.Port), conf) if err != nil { return nil, err } return client, nil } + +// dialSSH creates an SSH client over nuclei's policy-aware fastdialer. +func dialSSH(ctx context.Context, executionId, address string, config *ssh.ClientConfig) (*ssh.Client, error) { + if !protocolstate.IsHostAllowed(executionId, address) { + return nil, protocolstate.ErrHostDenied.Msgf(address) + } + dialers := protocolstate.GetDialersWithId(executionId) + if dialers == nil { + return nil, fmt.Errorf("dialers not initialized for %s", executionId) + } + conn, err := dialers.Fastdialer.Dial(ctx, "tcp", address) + if err != nil { + return nil, err + } + if config.Timeout != 0 { + _ = conn.SetDeadline(time.Now().Add(config.Timeout)) + } + clientConn, chans, reqs, err := ssh.NewClientConn(conn, address, config) + if err != nil { + _ = conn.Close() + return nil, err + } + // Clear the dial/handshake deadline so it doesn't leak into later + // reads/writes on the established connection. + _ = conn.SetDeadline(time.Time{}) + return ssh.NewClient(clientConn, chans, reqs), nil +} diff --git a/pkg/js/libs/telnet/telnet.go b/pkg/js/libs/telnet/telnet.go index eebfe5834e..ea98200222 100644 --- a/pkg/js/libs/telnet/telnet.go +++ b/pkg/js/libs/telnet/telnet.go @@ -64,6 +64,15 @@ type ( // const client = new telnet.TelnetClient(); // ``` TelnetClient struct{} + + // TelnetOptions defines the connection options for a Telnet server. + TelnetOptions struct { + Host string + Port int + Username string + Password string + Timeout int // Timeout is in seconds. + } ) // IsTelnet checks if a host is running a Telnet server. @@ -83,6 +92,9 @@ func isTelnet(ctx context.Context, executionId string, host string, port int) (I resp := IsTelnetResponse{} timeout := 5 * time.Second + if !protocolstate.IsHostAllowed(executionId, host) { + return resp, protocolstate.ErrHostDenied.Msgf(host) + } dialer := protocolstate.GetDialersWithId(executionId) if dialer == nil { return IsTelnetResponse{}, fmt.Errorf("dialers not initialized for %s", executionId) @@ -113,6 +125,8 @@ func isTelnet(ctx context.Context, executionId string, host string, port int) (I // Optionally provides username and password for authentication. // Returns state of connection. If the connection is successful, // the function will return true, otherwise false. +// +// Deprecated: prefer ConnectWithOptions for new templates. // @example // ```javascript // const telnet = require('nuclei/telnet'); @@ -120,6 +134,16 @@ func isTelnet(ctx context.Context, executionId string, host string, port int) (I // const connected = client.Connect('acme.com', 23, 'username', 'password'); // ``` func (c *TelnetClient) Connect(ctx context.Context, host string, port int, username string, password string) (bool, error) { + return c.ConnectWithOptions(ctx, TelnetOptions{ + Host: host, Port: port, Username: username, Password: password, + }) +} + +// ConnectWithOptions connects to Telnet using the supplied connection options. +func (c *TelnetClient) ConnectWithOptions(ctx context.Context, opts TelnetOptions) (bool, error) { + if opts.Host == "" || opts.Port <= 0 { + return false, fmt.Errorf("invalid host or port") + } executionId := ctx.Value("executionId").(string) dialer := protocolstate.GetDialersWithId(executionId) @@ -127,12 +151,12 @@ func (c *TelnetClient) Connect(ctx context.Context, host string, port int, usern return false, fmt.Errorf("dialers not initialized for %s", executionId) } - if !protocolstate.IsHostAllowed(executionId, host) { - return false, protocolstate.ErrHostDenied.Msgf(host) + if !protocolstate.IsHostAllowed(executionId, opts.Host) { + return false, protocolstate.ErrHostDenied.Msgf(opts.Host) } // Create TCP connection - conn, err := dialer.Fastdialer.Dial(ctx, "tcp", net.JoinHostPort(host, strconv.Itoa(port))) + conn, err := dialer.Fastdialer.Dial(ctx, "tcp", net.JoinHostPort(opts.Host, strconv.Itoa(opts.Port))) if err != nil { return false, err } @@ -144,12 +168,12 @@ func (c *TelnetClient) Connect(ctx context.Context, host string, port int, usern }() // Handle authentication if credentials provided - if username != "" && password != "" { + if opts.Username != "" && opts.Password != "" { // Set a timeout context for authentication - authCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + authCtx, cancel := context.WithTimeout(ctx, telnetTimeout(opts.Timeout)) defer cancel() - if err := client.Auth(authCtx, username, password); err != nil { + if err := client.Auth(authCtx, opts.Username, opts.Password); err != nil { return false, err } } @@ -157,6 +181,13 @@ func (c *TelnetClient) Connect(ctx context.Context, host string, port int, usern return true, nil } +func telnetTimeout(timeout int) time.Duration { + if timeout > 0 { + return time.Duration(timeout) * time.Second + } + return 10 * time.Second +} + // Info gathers information about the telnet server including encryption support. // Uses the telnetmini library's DetectEncryption helper function. // WARNING: The connection used for detection becomes unusable after this call. diff --git a/pkg/js/libs/telnet/telnet_options_test.go b/pkg/js/libs/telnet/telnet_options_test.go new file mode 100644 index 0000000000..b9032ee5fd --- /dev/null +++ b/pkg/js/libs/telnet/telnet_options_test.go @@ -0,0 +1,46 @@ +package telnet + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" + "github.com/projectdiscovery/nuclei/v3/pkg/types" +) + +func TestTelnetTimeout(t *testing.T) { + if got := telnetTimeout(2); got != 2*time.Second { + t.Fatalf("timeout = %s, want 2s", got) + } + if got := telnetTimeout(0); got != 10*time.Second { + t.Fatalf("default timeout = %s, want 10s", got) + } +} + +func TestConnectWithOptionsValidatesTarget(t *testing.T) { + connected, err := (&TelnetClient{}).ConnectWithOptions(context.Background(), TelnetOptions{Port: 23}) + if connected || err == nil || err.Error() != "invalid host or port" { + t.Fatalf("expected invalid target error, got connected=%t err=%v", connected, err) + } +} + +func TestConnectWithOptionsDeniesRestrictedLocalHostBeforeDial(t *testing.T) { + executionID := t.Name() + if err := protocolstate.Init(&types.Options{ + ExecutionId: executionID, + RestrictLocalNetworkAccess: true, + }); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { protocolstate.Close(executionID) }) + + ctx := context.WithValue(context.Background(), "executionId", executionID) // nolint:staticcheck + connected, err := (&TelnetClient{}).ConnectWithOptions(ctx, TelnetOptions{ + Host: "127.0.0.1", Port: 23, Timeout: 1, + }) + if connected || err == nil || !strings.Contains(err.Error(), "network policy") { + t.Fatalf("expected network-policy denial before dialing, got connected=%t err=%v", connected, err) + } +} diff --git a/pkg/js/libs/vnc/vnc.go b/pkg/js/libs/vnc/vnc.go index 2473b87a1b..21c4227b9f 100644 --- a/pkg/js/libs/vnc/vnc.go +++ b/pkg/js/libs/vnc/vnc.go @@ -36,12 +36,22 @@ type ( // log(toJSON(connected)); // ``` VNCClient struct{} + + // VNCOptions defines the connection options for a VNC server. + VNCOptions struct { + Host string + Port int + Password string + Timeout int // Timeout is in seconds. + } ) // Connect connects to VNC server using given password. // If connection and authentication is successful, it returns true. // If connection or authentication is unsuccessful, it returns false and error. // The connection is closed after the function returns. +// +// Deprecated: prefer ConnectWithOptions for new templates. // @example // ```javascript // const vnc = require('nuclei/vnc'); @@ -49,18 +59,22 @@ type ( // const connected = client.Connect('acme.com', 5900, 'password'); // ``` func (c *VNCClient) Connect(ctx context.Context, host string, port int, password string) (bool, error) { + return c.ConnectWithOptions(ctx, VNCOptions{Host: host, Port: port, Password: password}) +} + +// ConnectWithOptions connects to VNC using the supplied connection options. +func (c *VNCClient) ConnectWithOptions(ctx context.Context, opts VNCOptions) (bool, error) { executionId := ctx.Value("executionId").(string) - return connect(ctx, executionId, host, port, password) + return connectWithOptions(ctx, executionId, opts) } -// connect attempts to authenticate with a VNC server using the given password -func connect(ctx context.Context, executionId string, host string, port int, password string) (bool, error) { - if host == "" || port <= 0 { +func connectWithOptions(ctx context.Context, executionId string, opts VNCOptions) (bool, error) { + if opts.Host == "" || opts.Port <= 0 { return false, fmt.Errorf("invalid host or port") } - if !protocolstate.IsHostAllowed(executionId, host) { + if !protocolstate.IsHostAllowed(executionId, opts.Host) { // host is not valid according to network policy - return false, protocolstate.ErrHostDenied.Msgf(host) + return false, protocolstate.ErrHostDenied.Msgf(opts.Host) } dialer := protocolstate.GetDialersWithId(executionId) @@ -68,7 +82,7 @@ func connect(ctx context.Context, executionId string, host string, port int, pas return false, fmt.Errorf("dialers not initialized for %s", executionId) } - conn, err := dialer.Fastdialer.Dial(ctx, "tcp", net.JoinHostPort(host, strconv.Itoa(port))) + conn, err := dialer.Fastdialer.Dial(ctx, "tcp", net.JoinHostPort(opts.Host, strconv.Itoa(opts.Port))) if err != nil { return false, err } @@ -77,10 +91,10 @@ func connect(ctx context.Context, executionId string, host string, port int, pas }() // Set connection timeout - _ = conn.SetDeadline(time.Now().Add(10 * time.Second)) + _ = conn.SetDeadline(time.Now().Add(vncTimeout(opts.Timeout))) // Create VNC client config with password - vncConfig := vnclib.NewClientConfig(password) + vncConfig := vnclib.NewClientConfig(opts.Password) // Attempt to connect and authenticate c, err := vnclib.Connect(ctx, conn, vncConfig) @@ -98,6 +112,13 @@ func connect(ctx context.Context, executionId string, host string, port int, pas return true, nil } +func vncTimeout(timeout int) time.Duration { + if timeout > 0 { + return time.Duration(timeout) * time.Second + } + return 10 * time.Second +} + // isAuthError checks if the error is an authentication failure func isAuthError(err error) bool { if err == nil { @@ -128,6 +149,9 @@ func isVNC(ctx context.Context, executionId string, host string, port int) (IsVN resp := IsVNCResponse{} timeout := 5 * time.Second + if !protocolstate.IsHostAllowed(executionId, host) { + return resp, protocolstate.ErrHostDenied.Msgf(host) + } dialer := protocolstate.GetDialersWithId(executionId) if dialer == nil { return IsVNCResponse{}, fmt.Errorf("dialers not initialized for %s", executionId) diff --git a/pkg/js/libs/vnc/vnc_options_test.go b/pkg/js/libs/vnc/vnc_options_test.go new file mode 100644 index 0000000000..e3fce3e33a --- /dev/null +++ b/pkg/js/libs/vnc/vnc_options_test.go @@ -0,0 +1,45 @@ +package vnc + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" + "github.com/projectdiscovery/nuclei/v3/pkg/types" +) + +func TestVNCTimeout(t *testing.T) { + if got := vncTimeout(2); got != 2*time.Second { + t.Fatalf("timeout = %s, want 2s", got) + } + if got := vncTimeout(0); got != 10*time.Second { + t.Fatalf("default timeout = %s, want 10s", got) + } +} + +func TestConnectWithOptionsValidatesTarget(t *testing.T) { + connected, err := connectWithOptions(context.Background(), "test", VNCOptions{Port: 5900}) + if connected || err == nil || err.Error() != "invalid host or port" { + t.Fatalf("expected invalid target error, got connected=%t err=%v", connected, err) + } +} + +func TestConnectWithOptionsDeniesRestrictedLocalHostBeforeDial(t *testing.T) { + executionID := t.Name() + if err := protocolstate.Init(&types.Options{ + ExecutionId: executionID, + RestrictLocalNetworkAccess: true, + }); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { protocolstate.Close(executionID) }) + + connected, err := connectWithOptions(context.Background(), executionID, VNCOptions{ + Host: "127.0.0.1", Port: 5900, Timeout: 1, + }) + if connected || err == nil || !strings.Contains(err.Error(), "network policy") { + t.Fatalf("expected network-policy denial before dialing, got connected=%t err=%v", connected, err) + } +} diff --git a/pkg/js/utils/pgwrap/pgwrap.go b/pkg/js/utils/pgwrap/pgwrap.go index 18ed91a95a..213c06bd91 100644 --- a/pkg/js/utils/pgwrap/pgwrap.go +++ b/pkg/js/utils/pgwrap/pgwrap.go @@ -24,6 +24,9 @@ type pgDial struct { } func (p *pgDial) Dial(network, address string) (net.Conn, error) { + if !protocolstate.IsHostAllowed(p.executionId, address) { + return nil, protocolstate.ErrHostDenied.Msgf(address) + } dialers := protocolstate.GetDialersWithId(p.executionId) if dialers == nil { return nil, fmt.Errorf("dialers not initialized for %s", p.executionId) @@ -36,6 +39,9 @@ func (p *pgDial) Dial(network, address string) (net.Conn, error) { } func (p *pgDial) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) { + if !protocolstate.IsHostAllowed(p.executionId, address) { + return nil, protocolstate.ErrHostDenied.Msgf(address) + } dialers := protocolstate.GetDialersWithId(p.executionId) if dialers == nil { return nil, fmt.Errorf("dialers not initialized for %s", p.executionId) @@ -50,6 +56,9 @@ func (p *pgDial) DialTimeout(network, address string, timeout time.Duration) (ne } func (p *pgDial) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + if !protocolstate.IsHostAllowed(p.executionId, address) { + return nil, protocolstate.ErrHostDenied.Msgf(address) + } if ctx == nil { ctx = p.ctx } diff --git a/pkg/protocols/common/protocolstate/state.go b/pkg/protocols/common/protocolstate/state.go index 8278142c75..53b0cdfdb0 100644 --- a/pkg/protocols/common/protocolstate/state.go +++ b/pkg/protocols/common/protocolstate/state.go @@ -237,6 +237,9 @@ func initDialers(options *types.Options) error { if val := ctx.Value("executionId"); val != nil { executionId = val.(string) } + if !IsHostAllowed(executionId, addr) { + return nil, ErrHostDenied.Msgf(addr) + } dialer := GetDialersWithId(executionId) if dialer == nil { return nil, fmt.Errorf("dialers not initialized for %s", executionId)