diff --git a/CHANGELOG.md b/CHANGELOG.md index 74af6d48..c85150bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [6.20.0] - 2024-10-15 +### Added +- Add IncludeInsecureCiphers option to FTP backend. Fixes #204. + ## [6.19.0] - 2024-09-13 ### Added - Add ability to set file permissions after writing an SFTP file. Resolves #202. diff --git a/backend/ftp/doc.go b/backend/ftp/doc.go index 1aef9ead..c03ae68b 100644 --- a/backend/ftp/doc.go +++ b/backend/ftp/doc.go @@ -55,6 +55,7 @@ These methods are chainable: Protocol: ftp.ProtocolFTPES, DialTimeout: 15 * time.Second, DebugWriter: os.Stdout, + IncludeInsecureCiphers: true, }, ) @@ -148,5 +149,7 @@ DebugWriter *io.Writer* - captures FTP command details to any writer. DialTimeout *time.Duration - sets timeout for connecting only. DisableEPSV bool - Extended Passive mode (EPSV) is attempted by default. Set to true to use regular Passive mode (PASV). + +IncludeInsecureCiphers bool - If set to true, includes insecure cipher suites in the TLS configuration. */ package ftp diff --git a/backend/ftp/options.go b/backend/ftp/options.go index 947f7db1..00ea4554 100644 --- a/backend/ftp/options.go +++ b/backend/ftp/options.go @@ -17,12 +17,13 @@ import ( // Options struct implements the vfs.Options interface, providing optional parameters for creating and ftp filesystem. type Options struct { - Password string // env var VFS_FTP_PASSWORD - Protocol string // env var VFS_FTP_PROTOCOL - DisableEPSV *bool // env var VFS_DISABLE_EPSV - DebugWriter io.Writer - TLSConfig *tls.Config - DialTimeout time.Duration + Password string // env var VFS_FTP_PASSWORD + Protocol string // env var VFS_FTP_PROTOCOL + DisableEPSV *bool // env var VFS_DISABLE_EPSV + DebugWriter io.Writer + TLSConfig *tls.Config + DialTimeout time.Duration + IncludeInsecureCiphers bool } const ( @@ -162,6 +163,22 @@ func fetchTLSConfig(auth utils.Authority, opts Options) *tls.Config { ServerName: auth.Host(), } + if opts.IncludeInsecureCiphers { + var suites []uint16 + + // get default cipher suites + for _, suite := range tls.CipherSuites() { + suites = append(suites, suite.ID) + } + + // add insecure cipher suites + for _, suite := range tls.InsecureCipherSuites() { + suites = append(suites, suite.ID) + } + + tlsConfig.CipherSuites = suites + } + // override with Options, if any if opts.TLSConfig != nil { tlsConfig = opts.TLSConfig diff --git a/backend/ftp/options_test.go b/backend/ftp/options_test.go index 7fbb9de6..77be3d0a 100644 --- a/backend/ftp/options_test.go +++ b/backend/ftp/options_test.go @@ -215,10 +215,11 @@ func (s *optionsSuite) TestFetchTLSConfig() { } tests := []struct { - description string - authority string - options Options - expected *tls.Config + description string + authority string + options Options + expected *tls.Config + expectInsecureCipherSuites bool }{ { description: "check defaults", @@ -239,6 +240,20 @@ func (s *optionsSuite) TestFetchTLSConfig() { }, expected: cfg, }, + { + description: "include insecure cipher suites", + authority: "user@host.com", + options: Options{ + IncludeInsecureCiphers: true, + }, + expected: &tls.Config{ + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: true, //nolint:gosec + ClientSessionCache: tls.NewLRUClientSessionCache(0), + ServerName: "host.com", + }, + expectInsecureCipherSuites: true, + }, } for i := range tests { @@ -246,8 +261,29 @@ func (s *optionsSuite) TestFetchTLSConfig() { s.NoError(err, tests[i].description) tlsCfg := fetchTLSConfig(auth, tests[i].options) - s.Equal(tests[i].expected, tlsCfg, tests[i].description) + s.Equal(tests[i].expected.MinVersion, tlsCfg.MinVersion, tests[i].description) + s.Equal(tests[i].expected.InsecureSkipVerify, tlsCfg.InsecureSkipVerify, tests[i].description) + s.Equal(tests[i].expected.ClientSessionCache, tlsCfg.ClientSessionCache, tests[i].description) + s.Equal(tests[i].expected.ServerName, tlsCfg.ServerName, tests[i].description) + s.Equal(tests[i].expected.SessionTicketsDisabled, tlsCfg.SessionTicketsDisabled, tests[i].description) + + if tests[i].expectInsecureCipherSuites { + s.NotEmpty(tlsCfg.CipherSuites, tests[i].description) + s.True(containsInsecureCipherSuites(tlsCfg.CipherSuites), tests[i].description) + } + } +} + +func containsInsecureCipherSuites(suites []uint16) bool { + insecureSuites := tls.InsecureCipherSuites() + for _, s := range suites { + for _, insecureSuite := range insecureSuites { + if s == insecureSuite.ID { + return true + } + } } + return false } func (s *optionsSuite) TestFetchProtocol() {