mirror of
https://github.com/SynologyOpenSource/synology-csi.git
synced 2026-08-19 11:36:33 +00:00
The DSM WebAPI client disabled TLS certificate verification for all
HTTPS connections (InsecureSkipVerify: true), then sent the configured
DSM account and password to the endpoint. An attacker able to intercept,
redirect, or impersonate the DSM HTTPS endpoint could therefore obtain
the DSM credentials stored in the CSI client config / Kubernetes secret
(CWE-295 Improper Certificate Validation, OWASP A3:2017 Sensitive Data Exposure)
TLS certificate verification is now enabled by default. The client trusts
the system CA pool, so certificates signed by a public CA work without
extra configuration. Three optional client-info fields are added:
- tlsCACert: PEM CA cert to trust (for DSM self-signed certs);
merged with the system CA pool.
- tlsServerName: override the name checked during verification,
e.g. when connecting by IP.
- insecureSkipVerify: explicit opt-out that restores the old behavior;
logs a warning on every connection.
The new fields are propagated through all DSM construction sites
(service, synocli, and the HA GetAnotherController path).
BREAKING CHANGE: deployments using `https: true` against a DSM with a
self-signed certificate (the DSM default) will fail to connect after
upgrade until they set `tlsCACert`, `tlsServerName`, or (discouraged)
`insecureSkipVerify: true`.
Adds TLS tests covering default-reject, valid-CA accept, wrong-CA reject,
insecureSkipVerify opt-in, and the tlsServerName DNS-SAN scenarios.
255 lines
5.7 KiB
Go
255 lines
5.7 KiB
Go
/*
|
|
* Copyright 2021 Synology Inc.
|
|
*/
|
|
|
|
package webapi
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
|
|
"github.com/SynologyOpenSource/synology-csi/pkg/logger"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
type DSM struct {
|
|
Ip string
|
|
Port int
|
|
Username string
|
|
Password string
|
|
Sid string
|
|
Https bool
|
|
Controller string
|
|
TLSCACert string
|
|
TLSServerName string
|
|
InsecureSkipVerify bool
|
|
SystemInfo
|
|
}
|
|
|
|
type SystemInfo struct {
|
|
Hostname string
|
|
SupportNvmeof bool
|
|
FirmwareVer string
|
|
}
|
|
|
|
type errData struct {
|
|
Code int `json:"code"`
|
|
}
|
|
|
|
type dsmApiResp struct {
|
|
Success bool `json:"success"`
|
|
Err errData `json:"error"`
|
|
}
|
|
|
|
type Response struct {
|
|
StatusCode int
|
|
ErrorCode int
|
|
Success bool
|
|
Data interface{}
|
|
}
|
|
|
|
func (dsm *DSM) newHTTPClient() (*http.Client, error) {
|
|
if !dsm.Https {
|
|
return &http.Client{}, nil
|
|
}
|
|
|
|
tlsCfg := &tls.Config{}
|
|
|
|
if dsm.TLSServerName != "" {
|
|
tlsCfg.ServerName = dsm.TLSServerName
|
|
}
|
|
|
|
if dsm.InsecureSkipVerify {
|
|
if dsm.TLSCACert != "" {
|
|
log.Warnf("tlsCACert is configured but will be ignored for DSM %s because insecureSkipVerify is true.", dsm.Ip)
|
|
}
|
|
log.Warnf("TLS certificate verification is disabled for DSM %s. "+
|
|
"This is insecure; provide tlsCACert instead.", dsm.Ip)
|
|
tlsCfg.InsecureSkipVerify = true
|
|
} else if dsm.TLSCACert != "" {
|
|
pool, err := x509.SystemCertPool()
|
|
if err != nil {
|
|
log.Warnf("Failed to load system CA pool for DSM %s, falling back to empty pool: %v", dsm.Ip, err)
|
|
pool = x509.NewCertPool()
|
|
}
|
|
if ok := pool.AppendCertsFromPEM([]byte(dsm.TLSCACert)); !ok {
|
|
return nil, fmt.Errorf("failed to parse TLS CA certificate for DSM %s", dsm.Ip)
|
|
}
|
|
tlsCfg.RootCAs = pool
|
|
}
|
|
|
|
tr := http.DefaultTransport.(*http.Transport).Clone()
|
|
tr.TLSClientConfig = tlsCfg
|
|
|
|
return &http.Client{Transport: tr}, nil
|
|
}
|
|
|
|
func (dsm *DSM) sendRequest(data string, apiTemplate interface{}, params url.Values, cgiPath string) (Response, error) {
|
|
resp, err := dsm.sendRequestWithoutConnectionCheck(data, apiTemplate, params, cgiPath)
|
|
if err != nil && (resp.ErrorCode == 105 || resp.ErrorCode == 106 || resp.ErrorCode == 119) { // 105: WEBAPI_ERR_NO_PERMISSION, 106: session timeout, 119: WEBAPI_ERR_SID_NOT_FOUND
|
|
// Re-login
|
|
if err := dsm.Login(); err != nil {
|
|
return Response{}, fmt.Errorf("Failed to re-login to DSM: [%s]. err: %v", dsm.Ip, err)
|
|
}
|
|
log.Info("Re-login succeeded.")
|
|
return dsm.sendRequestWithoutConnectionCheck(data, apiTemplate, params, cgiPath)
|
|
}
|
|
|
|
return resp, err
|
|
}
|
|
|
|
func (dsm *DSM) sendRequestWithoutConnectionCheck(data string, apiTemplate interface{}, params url.Values, cgiPath string) (Response, error) {
|
|
client, err := dsm.newHTTPClient()
|
|
if err != nil {
|
|
return Response{}, err
|
|
}
|
|
|
|
var req *http.Request
|
|
var cgiUrl string
|
|
|
|
// Ex: http://10.12.12.14:5000/webapi/auth.cgi
|
|
if dsm.Https {
|
|
cgiUrl = fmt.Sprintf("https://%s:%d/%s", dsm.Ip, dsm.Port, cgiPath)
|
|
} else {
|
|
cgiUrl = fmt.Sprintf("http://%s:%d/%s", dsm.Ip, dsm.Port, cgiPath)
|
|
}
|
|
|
|
baseUrl, err := url.Parse(cgiUrl)
|
|
if err != nil {
|
|
return Response{}, err
|
|
}
|
|
|
|
baseUrl.RawQuery = params.Encode()
|
|
|
|
if logger.WebapiDebug {
|
|
log.Debugln(baseUrl.RawQuery)
|
|
}
|
|
|
|
if data != "" {
|
|
req, err = http.NewRequest("POST", baseUrl.String(), nil)
|
|
} else {
|
|
req, err = http.NewRequest("GET", baseUrl.String(), nil)
|
|
}
|
|
|
|
if dsm.Sid != "" {
|
|
cookie := http.Cookie{Name: "id", Value: dsm.Sid}
|
|
req.AddCookie(&cookie)
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return Response{}, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// For debug print text body
|
|
var bodyText []byte
|
|
if logger.WebapiDebug {
|
|
bodyText, err = ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return Response{}, err
|
|
}
|
|
s := string(bodyText)
|
|
log.Debugln(s)
|
|
}
|
|
|
|
if resp.StatusCode != 200 && resp.StatusCode != 302 {
|
|
return Response{}, fmt.Errorf("Bad response status code: %d", resp.StatusCode)
|
|
}
|
|
|
|
// Strip data json data from response
|
|
type envelop struct {
|
|
dsmApiResp
|
|
Data json.RawMessage `json:"data"`
|
|
}
|
|
|
|
e := envelop{}
|
|
var outResp Response
|
|
|
|
if logger.WebapiDebug {
|
|
if err := json.Unmarshal(bodyText, &e); err != nil {
|
|
return Response{}, err
|
|
}
|
|
} else {
|
|
decoder := json.NewDecoder(resp.Body)
|
|
|
|
if err := decoder.Decode(&e); err != nil {
|
|
return Response{}, err
|
|
}
|
|
}
|
|
outResp.Success = e.Success
|
|
outResp.ErrorCode = e.Err.Code
|
|
outResp.StatusCode = resp.StatusCode
|
|
|
|
if !e.Success {
|
|
return outResp, fmt.Errorf("DSM Api error. Error code:%d", outResp.ErrorCode)
|
|
}
|
|
|
|
if e.Data != nil {
|
|
if err := json.Unmarshal(e.Data, apiTemplate); err != nil {
|
|
return Response{}, err
|
|
}
|
|
}
|
|
|
|
outResp.Data = apiTemplate
|
|
|
|
if err != nil {
|
|
return Response{}, err
|
|
}
|
|
|
|
return outResp, nil
|
|
}
|
|
|
|
// Login by given user name and password
|
|
func (dsm *DSM) Login() error {
|
|
params := url.Values{}
|
|
params.Add("api", "SYNO.API.Auth")
|
|
params.Add("method", "login")
|
|
params.Add("version", "3")
|
|
params.Add("account", dsm.Username)
|
|
params.Add("passwd", dsm.Password)
|
|
params.Add("format", "sid")
|
|
|
|
type LoginResp struct {
|
|
Sid string `json:"sid"`
|
|
}
|
|
|
|
resp, err := dsm.sendRequestWithoutConnectionCheck("", &LoginResp{}, params, "webapi/auth.cgi")
|
|
if err != nil {
|
|
r, _ := regexp.Compile("passwd=.*&")
|
|
temp := r.ReplaceAllString(err.Error(), "")
|
|
|
|
return fmt.Errorf("%s", temp)
|
|
}
|
|
|
|
loginResp, ok := resp.Data.(*LoginResp)
|
|
if !ok {
|
|
return fmt.Errorf("Failed to assert response to %T", &LoginResp{})
|
|
}
|
|
dsm.Sid = loginResp.Sid
|
|
|
|
return nil
|
|
}
|
|
|
|
// Logout on current IP and reset the synoToken
|
|
func (dsm *DSM) Logout() error {
|
|
params := url.Values{}
|
|
params.Add("api", "SYNO.API.Auth")
|
|
params.Add("method", "logout")
|
|
params.Add("version", "1")
|
|
|
|
_, err := dsm.sendRequestWithoutConnectionCheck("", &struct{}{}, params, "webapi/entry.cgi")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dsm.Sid = ""
|
|
|
|
return nil
|
|
}
|