fix(cli): protect Go install credentials
This commit is contained in:
1 parent
3488ab778c
commit
996672a6a7
16 files changed
+1128
-79
No files matched your search
@@ -194,7 +194,7 @@ func prepareDroidInstallationChanges(
|
||||
mode os.FileMode
|
||||
}{
|
||||
{filepath.Join(agentDirectory, "config.json"), "Elydora runtime config", runtimeConfig, 0600},
|
||||
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", []byte(config.PrivateKey + "\n"), 0600},
|
||||
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", []byte(config.PrivateKey), 0600},
|
||||
{auditPath, "Elydora audit runtime", []byte(buildHookScript(droidAgentKey, config.AgentID)), 0700},
|
||||
}
|
||||
changes := make([]*fileChange, 0, len(runtimeChanges)+len(rendered))
|
||||
|
||||
@@ -2,71 +2,8 @@ package plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// GenerateHookScript writes a self-contained Node.js hook script to disk.
|
||||
// The script reads config/key from ~/.elydora/<agentId>/ at runtime, persists
|
||||
// chain state, and performs fire-and-forget EOR submission with inline Ed25519 crypto.
|
||||
// It always exits 0 so it never blocks the host agent.
|
||||
func GenerateHookScript(destPath string, config InstallConfig) error {
|
||||
dir := filepath.Dir(destPath)
|
||||
if err := EnsurePrivateDirectory(dir); err != nil {
|
||||
return fmt.Errorf("create hook script directory: %w", err)
|
||||
}
|
||||
|
||||
// Write the agent config and key files for runtime reading
|
||||
if err := writeAgentConfig(config); err != nil {
|
||||
return fmt.Errorf("write agent config: %w", err)
|
||||
}
|
||||
|
||||
script := buildHookScript(config.AgentName, config.AgentID)
|
||||
if err := os.WriteFile(destPath, []byte(script), 0755); err != nil {
|
||||
return fmt.Errorf("write hook script: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeAgentConfig persists the agent configuration and private key to
|
||||
// ~/.elydora/<agentId>/config.json and ~/.elydora/<agentId>/private.key so that
|
||||
// the generated hook script can read them at runtime without embedding secrets.
|
||||
func writeAgentConfig(config InstallConfig) error {
|
||||
agentDir, err := PrepareAgentRuntimeDirectory(config.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
baseURL := config.BaseURL
|
||||
if baseURL == "" {
|
||||
baseURL = "https://api.elydora.com"
|
||||
}
|
||||
|
||||
configJSON := fmt.Sprintf(`{
|
||||
"org_id": %q,
|
||||
"agent_id": %q,
|
||||
"kid": %q,
|
||||
"base_url": %q,
|
||||
"token": %q,
|
||||
"agent_name": %q
|
||||
}
|
||||
`, config.OrgID, config.AgentID, config.KID, baseURL, config.Token, config.AgentName)
|
||||
|
||||
configPath := filepath.Join(agentDir, "config.json")
|
||||
// Note: 0600 permissions are a no-op on Windows; Go's os.Chmod only handles the read-only bit on Windows.
|
||||
if err := os.WriteFile(configPath, []byte(configJSON), 0600); err != nil {
|
||||
return fmt.Errorf("write agent config: %w", err)
|
||||
}
|
||||
|
||||
keyPath := filepath.Join(agentDir, "private.key")
|
||||
// Note: 0600 permissions are a no-op on Windows; Go's os.Chmod only handles the read-only bit on Windows.
|
||||
if err := os.WriteFile(keyPath, []byte(config.PrivateKey+"\n"), 0600); err != nil {
|
||||
return fmt.Errorf("write agent key: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateGuardScript returns a self-contained Node.js guard script that checks
|
||||
// whether the agent is frozen and blocks tool execution if so.
|
||||
// It caches the agent status locally (60s TTL) to avoid hitting the API on every
|
||||
@@ -91,6 +28,8 @@ const CONFIG_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'config.json');
|
||||
const STATUS_CACHE_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'status-cache.json');
|
||||
const CACHE_TTL_MS = 60000; // 60 seconds
|
||||
|
||||
%s
|
||||
|
||||
// Consume stdin so the parent process doesn't block on a full pipe
|
||||
process.stdin.resume();
|
||||
process.stdin.on('data', () => {});
|
||||
@@ -99,7 +38,11 @@ async function main() {
|
||||
// Read agent config
|
||||
let config;
|
||||
try {
|
||||
config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
||||
config = JSON.parse(readProtectedFile(
|
||||
CONFIG_PATH,
|
||||
'Agent config',
|
||||
MAX_PROTECTED_CONFIG_BYTES,
|
||||
).toString('utf-8'));
|
||||
} catch {
|
||||
return; // Can't read config — fail-open (allow)
|
||||
}
|
||||
@@ -166,7 +109,7 @@ async function main() {
|
||||
}
|
||||
|
||||
main();
|
||||
`, agentName, agentName, agentId)
|
||||
`, agentName, agentName, agentId, protectedRuntimeFileReader)
|
||||
}
|
||||
|
||||
func buildHookScript(agentName string, agentId string) string {
|
||||
@@ -191,6 +134,21 @@ const KEY_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'private.key');
|
||||
const CHAIN_STATE_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'chain-state.json');
|
||||
const ERROR_LOG_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'error.log');
|
||||
|
||||
%s
|
||||
|
||||
function parsePrivateKey(raw) {
|
||||
let value = raw.toString('utf-8');
|
||||
if (value.endsWith('\r\n')) {
|
||||
value = value.slice(0, -2);
|
||||
} else if (value.endsWith('\n')) {
|
||||
value = value.slice(0, -1);
|
||||
}
|
||||
if (!value || value.includes('\r') || value.includes('\n') || value.includes('\0')) {
|
||||
throw new Error('Private key file must contain exactly one line');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Embedded crypto (from @elydora/sdk src/crypto.ts + src/utils.ts)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -369,8 +327,12 @@ async function main() {
|
||||
let config;
|
||||
let privateKey;
|
||||
try {
|
||||
config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
||||
privateKey = fs.readFileSync(KEY_PATH, 'utf-8').trim();
|
||||
config = JSON.parse(readProtectedFile(
|
||||
CONFIG_PATH,
|
||||
'Agent config',
|
||||
MAX_PROTECTED_CONFIG_BYTES,
|
||||
).toString('utf-8'));
|
||||
privateKey = parsePrivateKey(readProtectedFile(KEY_PATH, 'Private key'));
|
||||
} catch (err) {
|
||||
logError(new Error('Failed to read agent config/key: ' + err.message));
|
||||
return;
|
||||
@@ -471,5 +433,5 @@ async function main() {
|
||||
}
|
||||
|
||||
main();
|
||||
`, agentName, agentName, agentId)
|
||||
`, agentName, agentName, agentId, protectedRuntimeFileReader)
|
||||
}
|
||||
@@ -153,7 +153,7 @@ func prepareQwenInstallationChanges(
|
||||
mode os.FileMode
|
||||
}{
|
||||
{filepath.Join(agentDirectory, "config.json"), "Elydora runtime config", runtimeConfig, 0600},
|
||||
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", []byte(config.PrivateKey + "\n"), 0600},
|
||||
{filepath.Join(agentDirectory, "private.key"), "Elydora private key", []byte(config.PrivateKey), 0600},
|
||||
{auditPath, "Elydora audit runtime", []byte(buildHookScript(qwenAgentKey, config.AgentID)), 0700},
|
||||
}
|
||||
changes := make([]*fileChange, 0, len(items)+1)
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
maxProtectedSecretBytes = 64 * 1024
|
||||
maxRuntimeConfigBytes = 512 * 1024
|
||||
)
|
||||
|
||||
type agentRuntimeConfig struct {
|
||||
OrgID string `json:"org_id"`
|
||||
AgentID string `json:"agent_id"`
|
||||
KID string `json:"kid"`
|
||||
BaseURL string `json:"base_url"`
|
||||
Token string `json:"token"`
|
||||
AgentName string `json:"agent_name"`
|
||||
}
|
||||
|
||||
// WriteRuntimeFileAtomic replaces one managed runtime file through a flushed,
|
||||
// same-directory temporary file.
|
||||
func WriteRuntimeFileAtomic(
|
||||
path string,
|
||||
label string,
|
||||
content []byte,
|
||||
mode os.FileMode,
|
||||
) error {
|
||||
if err := validateRuntimeFileTarget(path, label); err != nil {
|
||||
return err
|
||||
}
|
||||
change, err := prepareFileChange(path, label, content, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeChanges([]*fileChange{change}, "write "+label, os.Rename)
|
||||
}
|
||||
|
||||
// GenerateHookScript atomically commits the agent config, private key, and
|
||||
// self-contained Node.js audit runtime.
|
||||
func GenerateHookScript(destPath string, config InstallConfig) error {
|
||||
return generateHookScriptWithRename(destPath, config, os.Rename)
|
||||
}
|
||||
|
||||
func generateHookScriptWithRename(
|
||||
destPath string,
|
||||
config InstallConfig,
|
||||
rename renameFunc,
|
||||
) error {
|
||||
agentDirectory, err := ResolveAgentRuntimeDirectory(config.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
baseURL := config.BaseURL
|
||||
if baseURL == "" {
|
||||
baseURL = "https://api.elydora.com"
|
||||
}
|
||||
encodedConfig, err := json.MarshalIndent(agentRuntimeConfig{
|
||||
OrgID: config.OrgID,
|
||||
AgentID: config.AgentID,
|
||||
KID: config.KID,
|
||||
BaseURL: baseURL,
|
||||
Token: config.Token,
|
||||
AgentName: config.AgentName,
|
||||
}, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode Elydora runtime config: %w", err)
|
||||
}
|
||||
encodedConfig = append(encodedConfig, '\n')
|
||||
if len(encodedConfig) > maxRuntimeConfigBytes {
|
||||
return fmt.Errorf(
|
||||
"Elydora runtime config exceeds %d bytes after JSON encoding",
|
||||
maxRuntimeConfigBytes,
|
||||
)
|
||||
}
|
||||
|
||||
items := []struct {
|
||||
path string
|
||||
label string
|
||||
content []byte
|
||||
mode os.FileMode
|
||||
}{
|
||||
{
|
||||
path: filepath.Join(agentDirectory, "config.json"), label: "Elydora runtime config",
|
||||
content: encodedConfig, mode: 0600,
|
||||
},
|
||||
{
|
||||
path: filepath.Join(agentDirectory, "private.key"), label: "Elydora private key",
|
||||
content: []byte(config.PrivateKey), mode: 0600,
|
||||
},
|
||||
{
|
||||
path: destPath, label: "Elydora audit runtime",
|
||||
content: []byte(buildHookScript(config.AgentName, config.AgentID)), mode: 0700,
|
||||
},
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
if err := validateRuntimeFileTarget(item.path, item.label); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := PrepareAgentRuntimeDirectory(config.AgentID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := EnsurePrivateDirectory(filepath.Dir(destPath)); err != nil {
|
||||
return fmt.Errorf("create hook script directory: %w", err)
|
||||
}
|
||||
|
||||
changes := make([]*fileChange, 0, len(items))
|
||||
for _, item := range items {
|
||||
change, prepareErr := prepareFileChange(
|
||||
item.path,
|
||||
item.label,
|
||||
item.content,
|
||||
item.mode,
|
||||
)
|
||||
if prepareErr != nil {
|
||||
return prepareErr
|
||||
}
|
||||
changes = append(changes, change)
|
||||
}
|
||||
if err := writeChanges(changes, "write Elydora agent runtime", rename); err != nil {
|
||||
return fmt.Errorf("commit Elydora agent runtime: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRuntimeFileTarget(path, label string) error {
|
||||
info, err := os.Lstat(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspect %s at %s: %w", label, path, err)
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
|
||||
return fmt.Errorf("%s path is not a physical file: %s", label, path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func setRuntimeFilesTestHome(t *testing.T) string {
|
||||
t.Helper()
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
return home
|
||||
}
|
||||
|
||||
func runtimeFilesTestConfig() InstallConfig {
|
||||
return InstallConfig{
|
||||
AgentName: "opencode",
|
||||
OrgID: "org-1",
|
||||
AgentID: "agent-1",
|
||||
PrivateKey: "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc",
|
||||
KID: "key-1",
|
||||
Token: "ely_test_token",
|
||||
BaseURL: "https://api.elydora.test",
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateHookScriptCommitsProtectedRuntimeFiles(t *testing.T) {
|
||||
home := setRuntimeFilesTestHome(t)
|
||||
config := runtimeFilesTestConfig()
|
||||
agentDirectory := filepath.Join(home, ".elydora", config.AgentID)
|
||||
hookPath := filepath.Join(agentDirectory, "hook.js")
|
||||
|
||||
if err := GenerateHookScript(hookPath, config); err != nil {
|
||||
t.Fatalf("generate hook script: %v", err)
|
||||
}
|
||||
|
||||
keyPath := filepath.Join(agentDirectory, "private.key")
|
||||
key, err := os.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read private key: %v", err)
|
||||
}
|
||||
if string(key) != config.PrivateKey {
|
||||
t.Fatalf("private key content changed")
|
||||
}
|
||||
hook, err := os.ReadFile(hookPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read hook runtime: %v", err)
|
||||
}
|
||||
if strings.Contains(string(hook), config.PrivateKey) {
|
||||
t.Fatalf("hook runtime embeds the private key")
|
||||
}
|
||||
if !strings.Contains(string(hook), "readProtectedFile") {
|
||||
t.Fatalf("hook runtime is missing protected file validation")
|
||||
}
|
||||
if runtime.GOOS != "windows" {
|
||||
assertRuntimeFileMode(t, filepath.Join(agentDirectory, "config.json"), 0600)
|
||||
assertRuntimeFileMode(t, keyPath, 0600)
|
||||
assertRuntimeFileMode(t, hookPath, 0700)
|
||||
}
|
||||
assertNoRuntimeStagingFiles(t, agentDirectory)
|
||||
}
|
||||
|
||||
func TestGenerateHookScriptSupportsMaxEscapedCredentialInput(t *testing.T) {
|
||||
home := setRuntimeFilesTestHome(t)
|
||||
config := runtimeFilesTestConfig()
|
||||
config.Token = strings.Repeat("\x01", 64*1024)
|
||||
agentDirectory := filepath.Join(home, ".elydora", config.AgentID)
|
||||
hookPath := filepath.Join(agentDirectory, "hook.js")
|
||||
|
||||
if err := GenerateHookScript(hookPath, config); err != nil {
|
||||
t.Fatalf("generate hook script with maximum escaped token: %v", err)
|
||||
}
|
||||
info, err := os.Stat(filepath.Join(agentDirectory, "config.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("stat runtime config: %v", err)
|
||||
}
|
||||
if info.Size() <= 64*1024 || info.Size() > maxRuntimeConfigBytes {
|
||||
t.Fatalf("encoded runtime config size = %d", info.Size())
|
||||
}
|
||||
hook, err := os.ReadFile(hookPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read hook runtime: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(hook), "MAX_PROTECTED_CONFIG_BYTES") {
|
||||
t.Fatalf("hook runtime is missing the config size contract")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateHookScriptRejectsOversizedRuntimeConfig(t *testing.T) {
|
||||
home := setRuntimeFilesTestHome(t)
|
||||
config := runtimeFilesTestConfig()
|
||||
config.Token = strings.Repeat("\x01", maxRuntimeConfigBytes)
|
||||
agentDirectory := filepath.Join(home, ".elydora", config.AgentID)
|
||||
|
||||
err := GenerateHookScript(filepath.Join(agentDirectory, "hook.js"), config)
|
||||
if err == nil || !strings.Contains(err.Error(), "exceeds") {
|
||||
t.Fatalf("generate oversized runtime config error = %v", err)
|
||||
}
|
||||
if _, statErr := os.Lstat(agentDirectory); !errors.Is(statErr, os.ErrNotExist) {
|
||||
t.Fatalf("oversized config created runtime state: %v", statErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateHookScriptRollsBackRuntimeTransaction(t *testing.T) {
|
||||
home := setRuntimeFilesTestHome(t)
|
||||
config := runtimeFilesTestConfig()
|
||||
agentDirectory := filepath.Join(home, ".elydora", config.AgentID)
|
||||
if err := os.MkdirAll(agentDirectory, 0700); err != nil {
|
||||
t.Fatalf("create runtime directory: %v", err)
|
||||
}
|
||||
paths := []string{
|
||||
filepath.Join(agentDirectory, "config.json"),
|
||||
filepath.Join(agentDirectory, "private.key"),
|
||||
filepath.Join(agentDirectory, "hook.js"),
|
||||
}
|
||||
for _, path := range paths {
|
||||
if err := os.WriteFile(path, []byte("original:"+filepath.Base(path)), 0600); err != nil {
|
||||
t.Fatalf("write original runtime file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
commits := 0
|
||||
rename := func(source, destination string) error {
|
||||
if strings.HasSuffix(source, ".tmp") {
|
||||
commits++
|
||||
if commits == 2 {
|
||||
return errors.New("injected runtime commit failure")
|
||||
}
|
||||
}
|
||||
return os.Rename(source, destination)
|
||||
}
|
||||
err := generateHookScriptWithRename(paths[2], config, rename)
|
||||
if err == nil || !strings.Contains(err.Error(), "injected runtime commit failure") {
|
||||
t.Fatalf("generate hook script error = %v", err)
|
||||
}
|
||||
for _, path := range paths {
|
||||
raw, readErr := os.ReadFile(path)
|
||||
if readErr != nil {
|
||||
t.Fatalf("read restored runtime file: %v", readErr)
|
||||
}
|
||||
want := "original:" + filepath.Base(path)
|
||||
if string(raw) != want {
|
||||
t.Fatalf("restored %s = %q, want %q", path, raw, want)
|
||||
}
|
||||
}
|
||||
assertNoRuntimeStagingFiles(t, agentDirectory)
|
||||
}
|
||||
|
||||
func TestGenerateHookScriptRejectsLinkedSecretTarget(t *testing.T) {
|
||||
home := setRuntimeFilesTestHome(t)
|
||||
config := runtimeFilesTestConfig()
|
||||
agentDirectory := filepath.Join(home, ".elydora", config.AgentID)
|
||||
if err := os.MkdirAll(agentDirectory, 0700); err != nil {
|
||||
t.Fatalf("create runtime directory: %v", err)
|
||||
}
|
||||
target := filepath.Join(t.TempDir(), "private.key")
|
||||
if err := os.WriteFile(target, []byte("external"), 0600); err != nil {
|
||||
t.Fatalf("write linked target: %v", err)
|
||||
}
|
||||
if err := os.Symlink(target, filepath.Join(agentDirectory, "private.key")); err != nil {
|
||||
t.Skipf("file symbolic links unavailable: %v", err)
|
||||
}
|
||||
|
||||
err := GenerateHookScript(filepath.Join(agentDirectory, "hook.js"), config)
|
||||
if err == nil || !strings.Contains(err.Error(), "physical file") {
|
||||
t.Fatalf("generate hook script error = %v", err)
|
||||
}
|
||||
raw, readErr := os.ReadFile(target)
|
||||
if readErr != nil || string(raw) != "external" {
|
||||
t.Fatalf("linked target changed: %q, %v", raw, readErr)
|
||||
}
|
||||
}
|
||||
|
||||
func assertRuntimeFileMode(t *testing.T, path string, want os.FileMode) {
|
||||
t.Helper()
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Fatalf("stat runtime file: %v", err)
|
||||
}
|
||||
if got := info.Mode().Perm(); got != want {
|
||||
t.Fatalf("mode for %s = %04o, want %04o", path, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func assertNoRuntimeStagingFiles(t *testing.T, directory string) {
|
||||
t.Helper()
|
||||
entries, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
t.Fatalf("read runtime directory: %v", err)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if strings.HasPrefix(entry.Name(), ".") &&
|
||||
(strings.HasSuffix(entry.Name(), ".tmp") || strings.HasSuffix(entry.Name(), ".rollback")) {
|
||||
t.Fatalf("staging file remains: %s", entry.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package plugins
|
||||
|
||||
import "fmt"
|
||||
|
||||
var protectedRuntimeFileReader = fmt.Sprintf(`const MAX_PROTECTED_SECRET_BYTES = %d;
|
||||
const MAX_PROTECTED_CONFIG_BYTES = %d;
|
||||
|
||||
function readProtectedFile(filePath, label, maxBytes = MAX_PROTECTED_SECRET_BYTES) {
|
||||
const before = fs.lstatSync(filePath);
|
||||
if (!before.isFile() || before.isSymbolicLink()) {
|
||||
throw new Error(label + ' path is not a physical file: ' + filePath);
|
||||
}
|
||||
if (before.size > maxBytes) {
|
||||
throw new Error(label + ' exceeds the size limit: ' + filePath);
|
||||
}
|
||||
if (process.platform !== 'win32' && (before.mode & 0o077) !== 0) {
|
||||
throw new Error(label + ' permissions are too broad: ' + filePath);
|
||||
}
|
||||
|
||||
let flags = fs.constants.O_RDONLY;
|
||||
if (process.platform !== 'win32' && typeof fs.constants.O_NOFOLLOW === 'number') {
|
||||
flags |= fs.constants.O_NOFOLLOW;
|
||||
}
|
||||
const descriptor = fs.openSync(filePath, flags);
|
||||
try {
|
||||
const after = fs.fstatSync(descriptor);
|
||||
if (!after.isFile()) {
|
||||
throw new Error(label + ' path is not a physical file: ' + filePath);
|
||||
}
|
||||
if (after.size > maxBytes) {
|
||||
throw new Error(label + ' exceeds the size limit: ' + filePath);
|
||||
}
|
||||
if (process.platform !== 'win32' && (after.mode & 0o077) !== 0) {
|
||||
throw new Error(label + ' permissions are too broad: ' + filePath);
|
||||
}
|
||||
if (before.dev !== after.dev || before.ino !== after.ino) {
|
||||
throw new Error(label + ' changed while opening: ' + filePath);
|
||||
}
|
||||
const raw = fs.readFileSync(descriptor);
|
||||
if (raw.length > maxBytes) {
|
||||
throw new Error(label + ' exceeds the size limit: ' + filePath);
|
||||
}
|
||||
return raw;
|
||||
} finally {
|
||||
fs.closeSync(descriptor);
|
||||
}
|
||||
}
|
||||
`, maxProtectedSecretBytes, maxRuntimeConfigBytes)
|
||||
Reference in new issue
Block a user