fix(security): harden self-hosted SECRET_KEY bootstrap (#36049)

Co-authored-by: EndlessLucky <66432853+EndlessLucky@users.noreply.github.com>
This commit is contained in:
-LAN-
2026-05-12 05:35:24 +00:00
committed by GitHub
co-authored by EndlessLucky
parent 1a93af5cd0
commit cbedcd2882
16 changed files with 209 additions and 84 deletions
@@ -8,6 +8,47 @@ from yarl import URL
from configs.app_config import DifyConfig
def _set_basic_config_env(monkeypatch: pytest.MonkeyPatch) -> None:
os.environ.clear()
monkeypatch.setenv("CONSOLE_API_URL", "https://example.com")
monkeypatch.setenv("CONSOLE_WEB_URL", "https://example.com")
monkeypatch.setenv("DB_TYPE", "postgresql")
monkeypatch.setenv("DB_USERNAME", "postgres")
monkeypatch.setenv("DB_PASSWORD", "postgres")
monkeypatch.setenv("DB_HOST", "localhost")
monkeypatch.setenv("DB_PORT", "5432")
monkeypatch.setenv("DB_DATABASE", "dify")
def test_dify_config_keeps_secret_key_empty_when_missing(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
) -> None:
_set_basic_config_env(monkeypatch)
monkeypatch.delenv("SECRET_KEY", raising=False)
monkeypatch.setenv("OPENDAL_FS_ROOT", str(tmp_path))
config = DifyConfig(_env_file=None)
assert config.SECRET_KEY == ""
assert not hasattr(config, "OPENDAL_FS_ROOT")
assert not (tmp_path / ".dify_secret_key").exists()
def test_dify_config_preserves_explicit_secret_key(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
) -> None:
_set_basic_config_env(monkeypatch)
monkeypatch.setenv("SECRET_KEY", "explicit")
monkeypatch.setenv("OPENDAL_FS_ROOT", str(tmp_path))
config = DifyConfig(_env_file=None)
assert config.SECRET_KEY == "explicit"
assert not (tmp_path / ".dify_secret_key").exists()
def test_dify_config(monkeypatch: pytest.MonkeyPatch):
# clear system environment variables
os.environ.clear()
@@ -34,20 +34,6 @@ class TestDatasourceFileManager:
assert f"nonce={mock_urandom.return_value.hex()}" in signed_url
assert "sign=" in signed_url
@patch("core.datasource.datasource_file_manager.time.time")
@patch("core.datasource.datasource_file_manager.os.urandom")
@patch("core.datasource.datasource_file_manager.dify_config")
def test_sign_file_empty_secret(self, mock_config, mock_urandom, mock_time):
# Setup
mock_config.FILES_URL = "http://localhost:5001"
mock_config.SECRET_KEY = None # Empty secret
mock_time.return_value = 1700000000
mock_urandom.return_value = b"1234567890abcdef"
# Execute
signed_url = DatasourceFileManager.sign_file("file_id", ".png")
assert "sign=" in signed_url
@patch("core.datasource.datasource_file_manager.time.time")
@patch("core.datasource.datasource_file_manager.dify_config")
def test_verify_file(self, mock_config, mock_time):
@@ -76,25 +62,6 @@ class TestDatasourceFileManager:
mock_time.return_value = 1700000500 # 700 seconds after timestamp (300 is timeout)
assert DatasourceFileManager.verify_file(datasource_file_id, timestamp, nonce, encoded_sign) is False
@patch("core.datasource.datasource_file_manager.time.time")
@patch("core.datasource.datasource_file_manager.dify_config")
def test_verify_file_empty_secret(self, mock_config, mock_time):
# Setup
mock_config.SECRET_KEY = "" # Empty string secret
mock_config.FILES_ACCESS_TIMEOUT = 300
mock_time.return_value = 1700000000
datasource_file_id = "file_id_123"
timestamp = "1699999800"
nonce = "some_nonce"
# Calculate with empty secret
data_to_sign = f"file-preview|{datasource_file_id}|{timestamp}|{nonce}"
sign = hmac.new(b"", data_to_sign.encode(), hashlib.sha256).digest()
encoded_sign = base64.urlsafe_b64encode(sign).decode()
assert DatasourceFileManager.verify_file(datasource_file_id, timestamp, nonce, encoded_sign) is True
@patch("core.datasource.datasource_file_manager.db")
@patch("core.datasource.datasource_file_manager.storage")
@patch("core.datasource.datasource_file_manager.uuid4")
@@ -0,0 +1,74 @@
from __future__ import annotations
import pytest
from flask import Flask
from extensions import ext_set_secretkey
class InMemoryStorage:
def __init__(self, files: dict[str, bytes] | None = None) -> None:
self.files = files or {}
self.saved_files: list[tuple[str, bytes]] = []
def load_once(self, filename: str) -> bytes:
try:
return self.files[filename]
except KeyError:
raise FileNotFoundError(filename)
def save(self, filename: str, data: bytes) -> None:
self.files[filename] = data
self.saved_files.append((filename, data))
def test_init_app_uses_configured_secret_key(monkeypatch: pytest.MonkeyPatch) -> None:
secret_key = "configured-secret-key"
storage = InMemoryStorage()
monkeypatch.setattr("extensions.ext_set_secretkey.dify_config.SECRET_KEY", secret_key)
monkeypatch.setattr("configs.secret_key.storage", storage)
app = Flask(__name__)
app.config["SECRET_KEY"] = secret_key
ext_set_secretkey.init_app(app)
assert app.secret_key == secret_key
assert app.config["SECRET_KEY"] == secret_key
assert storage.saved_files == []
def test_init_app_generates_and_persists_secret_key_when_missing(
monkeypatch: pytest.MonkeyPatch,
) -> None:
storage = InMemoryStorage()
monkeypatch.setattr("extensions.ext_set_secretkey.dify_config.SECRET_KEY", "")
monkeypatch.setattr("configs.secret_key.storage", storage)
app = Flask(__name__)
app.config["SECRET_KEY"] = ""
ext_set_secretkey.init_app(app)
persisted_key = storage.files[".dify_secret_key"].decode("utf-8").strip()
assert persisted_key
assert storage.saved_files == [(".dify_secret_key", f"{persisted_key}\n".encode())]
assert persisted_key == ext_set_secretkey.dify_config.SECRET_KEY
assert persisted_key == app.config["SECRET_KEY"]
assert persisted_key == app.secret_key
def test_init_app_reuses_persisted_secret_key_when_missing(
monkeypatch: pytest.MonkeyPatch,
) -> None:
persisted_key = "persisted-secret-key"
storage = InMemoryStorage({".dify_secret_key": f"{persisted_key}\n".encode()})
monkeypatch.setattr("extensions.ext_set_secretkey.dify_config.SECRET_KEY", "")
monkeypatch.setattr("configs.secret_key.storage", storage)
app = Flask(__name__)
app.config["SECRET_KEY"] = ""
ext_set_secretkey.init_app(app)
assert persisted_key == ext_set_secretkey.dify_config.SECRET_KEY
assert persisted_key == app.config["SECRET_KEY"]
assert persisted_key == app.secret_key
assert storage.saved_files == []
+4 -19
View File
@@ -143,28 +143,13 @@ class TestPassportService:
assert str(exc_info.value) == "401 Unauthorized: Token has expired."
# Configuration tests
def test_should_handle_empty_secret_key(self):
"""Test behavior when SECRET_KEY is empty"""
def test_should_use_configured_secret_key_without_policy_validation(self):
"""Test that policy decisions are owned by config, not PassportService."""
with patch("libs.passport.dify_config") as mock_config:
mock_config.SECRET_KEY = ""
mock_config.SECRET_KEY = "configured"
service = PassportService()
# Empty secret key should still work but is insecure
payload = {"test": "data"}
token = service.issue(payload)
decoded = service.verify(token)
assert decoded == payload
def test_should_handle_none_secret_key(self):
"""Test behavior when SECRET_KEY is None"""
with patch("libs.passport.dify_config") as mock_config:
mock_config.SECRET_KEY = None
service = PassportService()
payload = {"test": "data"}
# JWT library will raise TypeError when secret is None
with pytest.raises((TypeError, jwt.exceptions.InvalidKeyError)):
service.issue(payload)
assert service.sk == "configured"
# Boundary condition tests
def test_should_handle_large_payload(self, passport_service):