mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-15 15:50:59 +00:00
fix(security): confine GetCertHash to known cert files (CWE-22)
Resolve CodeQL go/path-injection (alert #96): the certFile path from the getCertHash endpoint flowed straight into os.ReadFile, letting an authenticated request read arbitrary files by path. Validate it against an allow-list of certificate files the panel already references (inbound TLS certificateFile values plus the panel's own web cert) and read the config-sourced path rather than the caller-supplied one, breaking the taint flow while preserving arbitrary cert locations.
This commit is contained in:
@@ -1735,7 +1735,16 @@ func (s *ServerService) GetNewmldsa65() (any, error) {
|
|||||||
func (s *ServerService) GetCertHash(certFile string, certContent string) ([]string, error) {
|
func (s *ServerService) GetCertHash(certFile string, certContent string) ([]string, error) {
|
||||||
var certBytes []byte
|
var certBytes []byte
|
||||||
if path := strings.TrimSpace(certFile); path != "" {
|
if path := strings.TrimSpace(certFile); path != "" {
|
||||||
b, err := os.ReadFile(path)
|
// Guard against path traversal: only hash certificate files the panel
|
||||||
|
// already references in its own configuration (an inbound's TLS
|
||||||
|
// certificateFile or the panel's own web cert). The path handed to
|
||||||
|
// os.ReadFile comes from that allow-list, never directly from the
|
||||||
|
// caller-supplied value.
|
||||||
|
known, ok := s.resolveKnownCertFile(path)
|
||||||
|
if !ok {
|
||||||
|
return nil, common.NewError("certificate file is not referenced by any inbound or panel setting")
|
||||||
|
}
|
||||||
|
b, err := os.ReadFile(known)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1781,6 +1790,75 @@ func (s *ServerService) GetCertHash(certFile string, certContent string) ([]stri
|
|||||||
return hashes, nil
|
return hashes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolveKnownCertFile checks the caller-supplied certificate path against the
|
||||||
|
// set of certificate files the panel already references (inbound TLS configs
|
||||||
|
// plus the panel's own web cert) and, on a match, returns the path taken from
|
||||||
|
// that configuration — not the caller's value. This both confines reads to
|
||||||
|
// known certificates and breaks the user-input-to-filesystem taint flow.
|
||||||
|
func (s *ServerService) resolveKnownCertFile(certFile string) (string, bool) {
|
||||||
|
want := filepath.Clean(certFile)
|
||||||
|
for _, known := range s.knownCertFiles() {
|
||||||
|
if filepath.Clean(known) == want {
|
||||||
|
return known, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// knownCertFiles collects every certificate file path the panel legitimately
|
||||||
|
// references: the certificateFile of each inbound's TLS settings and the
|
||||||
|
// panel's own web TLS certificate.
|
||||||
|
func (s *ServerService) knownCertFiles() []string {
|
||||||
|
var files []string
|
||||||
|
if cert, err := s.settingService.GetCertFile(); err == nil {
|
||||||
|
if cert = strings.TrimSpace(cert); cert != "" {
|
||||||
|
files = append(files, cert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if inbounds, err := s.inboundService.GetAllInbounds(); err == nil {
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
files = collectCertFiles(inbound.StreamSettings, files)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectCertFiles walks a stream-settings JSON document and appends the value
|
||||||
|
// of every "certificateFile" field it finds (TLS settings may nest them under
|
||||||
|
// several keys depending on the security type).
|
||||||
|
func collectCertFiles(streamSettings string, out []string) []string {
|
||||||
|
streamSettings = strings.TrimSpace(streamSettings)
|
||||||
|
if streamSettings == "" {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
var parsed any
|
||||||
|
if err := json.Unmarshal([]byte(streamSettings), &parsed); err != nil {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
return walkCertFiles(parsed, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func walkCertFiles(node any, out []string) []string {
|
||||||
|
switch v := node.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
for key, val := range v {
|
||||||
|
if key == "certificateFile" {
|
||||||
|
if path, ok := val.(string); ok {
|
||||||
|
if path = strings.TrimSpace(path); path != "" {
|
||||||
|
out = append(out, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = walkCertFiles(val, out)
|
||||||
|
}
|
||||||
|
case []any:
|
||||||
|
for _, item := range v {
|
||||||
|
out = walkCertFiles(item, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// GetRemoteCertHash runs `xray tls ping <server>` to fetch the live certificate
|
// GetRemoteCertHash runs `xray tls ping <server>` to fetch the live certificate
|
||||||
// SHA-256 of a remote endpoint — the value to put in pinnedPeerCertSha256 (pcs)
|
// SHA-256 of a remote endpoint — the value to put in pinnedPeerCertSha256 (pcs)
|
||||||
// when pinning a server whose certificate file you don't hold (a CDN front, a
|
// when pinning a server whose certificate file you don't hold (a CDN front, a
|
||||||
|
|||||||
Reference in New Issue
Block a user