mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
fix: security hardening — zip-slip, credential leaks, env injection, path resolution
Critical:
- api/backup.go: add Zip Slip containment check on restore (reject .. escapes)
- api/backup.go: verify restored nwc.db exists before destroying current DB
High:
- lnclient/greenlight/provision.go: strip mnemonic from glcli output before
logging or returning in errors
- lnclient/greenlight/provision.go: reject newlines in GL_NOBODY_CRT/KEY
(env-injection guard)
Medium:
- lnclient/greenlight/provision.go: resolve python3 via exec.LookPath
instead of plain 'python3' from PATH
- service/gl_signer.go: reject empty network instead of silently defaulting
to mainnet
Low:
- lnclient/greenlight/extract_creds.py: set 0o700 on output directory
This commit is contained in:
parent
dfa572166c
commit
3e61135719
4 changed files with 37 additions and 3 deletions
|
|
@ -229,6 +229,14 @@ func (api *api) RestoreBackup(unlockPassword string, r io.Reader) error {
|
|||
extractZipEntry := func(zipFile *zip.File) error {
|
||||
fsFilePath := filepath.Join(workDir, "restore", filepath.FromSlash(zipFile.Name))
|
||||
|
||||
// Prevent Zip Slip: ensure the resolved path stays within the restore dir.
|
||||
cleaned := filepath.Clean(fsFilePath)
|
||||
restoreRoot := filepath.Join(workDir, "restore")
|
||||
if !strings.HasPrefix(cleaned, restoreRoot+string(filepath.Separator)) && cleaned != restoreRoot {
|
||||
return fmt.Errorf("zip entry escapes restore directory: %s", zipFile.Name)
|
||||
}
|
||||
fsFilePath = cleaned
|
||||
|
||||
if err = os.MkdirAll(filepath.Dir(fsFilePath), 0700); err != nil {
|
||||
return fmt.Errorf("failed to create directory for zip entry: %w", err)
|
||||
}
|
||||
|
|
@ -264,6 +272,15 @@ func (api *api) RestoreBackup(unlockPassword string, r io.Reader) error {
|
|||
go func() {
|
||||
logger.Logger.Info("Backup restored. Shutting down Alby Hub...")
|
||||
api.svc.Shutdown()
|
||||
|
||||
// Verify restored files exist before destroying current DB
|
||||
restoreDir := filepath.Join(workDir, "restore")
|
||||
nwcPath := filepath.Join(restoreDir, "nwc.db")
|
||||
if _, statErr := os.Stat(nwcPath); statErr != nil {
|
||||
logger.Logger.WithError(statErr).Error("Restored nwc.db not found — refusing to destroy current DB")
|
||||
return
|
||||
}
|
||||
|
||||
// ensure no -shm or -wal files exist as they will stop the restore
|
||||
for _, filename := range []string{"nwc.db", "nwc.db-shm", "nwc.db-wal"} {
|
||||
err = os.Remove(filepath.Join(workDir, filename))
|
||||
|
|
|
|||
|
|
@ -165,6 +165,7 @@ def main() -> int:
|
|||
return 1
|
||||
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
os.chmod(out_dir, 0o700)
|
||||
|
||||
files = {
|
||||
"client.pem": cert,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ const (
|
|||
deviceCredsDirName = "device-creds"
|
||||
)
|
||||
|
||||
// sanitizeOutput strips the mnemonic from glcli output (e.g. "New seed
|
||||
// generated from mnemonic: ...") before logging or returning in errors.
|
||||
var reMnemonic = regexp.MustCompile(`mnemonic:\s*\\S.*`) // rest of mnemonic line
|
||||
|
||||
// MnemonicToSeed32 matches gl-cli: mnemonic.to_seed("")[0..32], 12 words only.
|
||||
func MnemonicToSeed32(mnemonic string) ([]byte, error) {
|
||||
mnemonic = strings.TrimSpace(mnemonic)
|
||||
|
|
@ -115,6 +119,10 @@ func EnsureProvisioned(dataDir, network, glcliPath, nobodyCrt, nobodyKey, mnemon
|
|||
cmd := exec.Command(bin, args...)
|
||||
env := os.Environ()
|
||||
if nobodyCrt != "" {
|
||||
// guard against env-injection: reject paths with newlines
|
||||
if strings.ContainsAny(nobodyCrt, "\n\r") || strings.ContainsAny(nobodyKey, "\n\r") {
|
||||
return "", fmt.Errorf("GL_NOBODY_CRT/KEY must not contain newlines")
|
||||
}
|
||||
env = append(env, "GL_NOBODY_CRT="+nobodyCrt)
|
||||
}
|
||||
if nobodyKey != "" {
|
||||
|
|
@ -123,8 +131,10 @@ func EnsureProvisioned(dataDir, network, glcliPath, nobodyCrt, nobodyKey, mnemon
|
|||
cmd.Env = env
|
||||
out, err := cmd.CombinedOutput()
|
||||
s := string(out)
|
||||
// strip seed/mnemonic from output before logging or including in errors
|
||||
sanitized := reMnemonic.ReplaceAllString(s, "mnemonic: [redacted]")
|
||||
if err != nil {
|
||||
return s, fmt.Errorf("%w: %s", err, strings.TrimSpace(s))
|
||||
return s, fmt.Errorf("%w: %s", err, strings.TrimSpace(sanitized))
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
|
@ -172,7 +182,12 @@ func EnsureProvisioned(dataDir, network, glcliPath, nobodyCrt, nobodyKey, mnemon
|
|||
return "", "", fmt.Errorf("write embedded extract_creds: %w", err)
|
||||
}
|
||||
}
|
||||
cmd := exec.Command("python3", extractScript, credsBlob, credsDir)
|
||||
// resolve python3 via LookPath (not plain "python3" from PATH) for exec
|
||||
python3, err := exec.LookPath("python3")
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("python3 not found in PATH: %w", err)
|
||||
}
|
||||
cmd := exec.Command(python3, extractScript, credsBlob, credsDir)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("extract_creds failed: %w: %s", err, string(out))
|
||||
|
|
|
|||
|
|
@ -48,7 +48,8 @@ func (s *GreenlightSignerService) Start(ctx context.Context, dataDir, network, g
|
|||
s.dataDir = dataDir
|
||||
s.network = network
|
||||
if s.network == "" {
|
||||
s.network = "bitcoin"
|
||||
s.mu.Unlock()
|
||||
return fmt.Errorf("network required (set GREENLIGHT_NETWORK)")
|
||||
}
|
||||
s.glcli = glcliPath
|
||||
s.pidPath = filepath.Join(dataDir, "signer.pid")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue