Understanding the Symlink Security Check in DCG's System Configuration Loading

The symlink security check in DCG prevents privilege escalation by verifying that system configuration files are not symlinks pointing to user-writable directories before loading them.

DCG (destructive_command_guard) implements a layered configuration system that loads settings from environment, project, user, and system sources. When loading the system configuration layer from /etc/dcg/config.toml on Unix or %ProgramData%\dcg\config.toml on Windows, the application performs a critical symlink security check to prevent non-privileged users from hijacking administrator settings through symbolic link attacks.

The security mechanism operates during the read_config_file_bounded function execution, specifically when processing the ConfigSource::System layer.

Bounded File Reading

Before any security checks, DCG enforces a 1 MiB size limit on configuration files to prevent memory exhaustion attacks. The read_config_file_bounded function caps the file size before loading content into memory.

System Layer Detection

When the configuration source is identified as ConfigSource::System, DCG inspects the file metadata without following symbolic links using fs::symlink_metadata. This examination occurs in src/config.rs at lines 45-58:

if source == ConfigSource::System {
    match fs::symlink_metadata(path) {
        Ok(meta) if meta.file_type().is_symlink() => {
            if symlink_target_is_user_writable(path) {
                eprintln!(
                    "Warning: refusing to load system config '{}' — symlink to user-writable target",
                    path.display()
                );
                return None;
            }
        }
        // … other branches omitted …
    }
}

If the metadata indicates the path is a symlink, DCG invokes symlink_target_is_user_writable (defined at lines 12-33 in src/config.rs) to validate the target's security posture:

fn symlink_target_is_user_writable(path: &Path) -> bool {
    let Ok(target) = fs::canonicalize(path) else { return true };
    #[cfg(unix)]
    {
        use std::os::unix::fs::{MetadataExt, PermissionsExt};
        let parent = target.parent().unwrap_or(&target);
        let Ok(meta) = fs::metadata(parent) else { return true };
        meta.uid() != 0 || (meta.permissions().mode() & 0o022) != 0
    }
    #[cfg(not(unix))]
    { false }
}

The function resolves the symlink target using fs::canonicalize, then examines the parent directory's ownership and permissions. On Unix systems, the target is considered user-writable if the parent directory is not owned by root (UID 0) or if it has group or world-write permissions (mode bits 0o022). On non-Unix platforms, the check defaults to allowing the symlink since the attack surface differs.

Security Implications

The symlink security check serves critical protective functions:

  • Privilege escalation prevention: System configuration is intended for administrator control. Without this check, a regular user could create a symlink from /etc/dcg/config.toml to a user-controlled file, injecting arbitrary settings into a privileged configuration layer.
  • Fail-safe design: The check errs conservatively. Any error during target inspection—such as missing permissions or inaccessible paths—results in refusing to load the file, ensuring potentially compromised configurations are skipped.
  • Layered integrity preservation: Since DCG's configuration layers follow priority rules (environment > project > user > system), refusing a malicious system config preserves the intended security model without silently falling back to compromised settings.

Practical Examples

Attempting to load a system configuration through a user-writable symlink triggers the security warning:


# Create a user-writable file and a symlink under /etc/dcg

mkdir -p /tmp/user_config
echo 'check_updates = false' > /tmp/user_config/bad.toml
sudo ln -s /tmp/user_config/bad.toml /etc/dcg/config.toml

# Run dcg (it will emit a warning and skip the system config)

dcg --version

# → Warning: refusing to load system config '/etc/dcg/config.toml' — symlink to user-writable target

DCG proceeds with remaining configuration layers (user, project, environment) while skipping the compromised system layer.

Loading a Valid System Configuration

When the system configuration is a regular file owned by root, loading proceeds normally:


# Write a proper system config as root

sudo sh -c "echo 'check_updates = true' > /etc/dcg/config.toml"

# dcg now reads the file without warnings

dcg --version

# (no warning, system config applied)

Programmatic Usage

When using DCG's configuration API directly, the symlink check executes automatically:

use std::path::Path;
use dcg::config::{read_config_file_bounded, ConfigSource};

fn load_system_config() -> Option<String> {
    let sys_path = dcg::config::system_config_dir().join("config.toml");
    read_config_file_bounded(&sys_path, ConfigSource::System)
}

The read_config_file_bounded function automatically applies the symlink security check when the ConfigSource::System variant is passed.

Key Source Files

The symlink security logic resides in the following locations within the Dicklesworthstone/destructive_command_guard repository:

  • src/config.rs (lines 45-58): Contains the conditional guard that detects symlinks in system configuration paths and refuses to load user-writable targets.
  • src/config.rs (lines 12-33): Implements the symlink_target_is_user_writable helper function that examines directory ownership and permission bits on Unix systems.

Summary

  • The symlink security check in DCG prevents privilege escalation by validating system configuration files before loading.
  • Located in src/config.rs, the check uses fs::symlink_metadata to detect symlinks without following them, then validates targets using symlink_target_is_user_writable.
  • On Unix systems, targets are rejected if their parent directories are not owned by root (UID 0) or possess group/world-write permissions (mode 0o022).
  • When a suspicious symlink is detected, DCG emits a warning and returns None, causing the system configuration layer to be skipped while preserving higher-priority layers.
  • The check defaults to allowing symlinks on non-Unix platforms where the attack surface differs.

Frequently Asked Questions

The check triggers when DCG attempts to load the system configuration layer from /etc/dcg/config.toml (Unix) or %ProgramData%\dcg\config.toml (Windows). Specifically, when read_config_file_bounded is called with ConfigSource::System, it examines the file metadata to determine if the path is a symbolic link before reading contents.

DCG resolves the symlink using fs::canonicalize, then examines the parent directory's metadata. On Unix systems, the symlink_target_is_user_writable function checks if the parent directory's UID is not 0 (root) or if the permission mode has group or world-write bits set (0o022). If either condition is true, the target is considered user-writable. On non-Unix systems, the function returns false, allowing the symlink.

When DCG detects a symlink pointing to a user-writable target, it prints a warning message to stderr indicating the refusal to load the system config, then returns None from the configuration loader. This causes DCG to skip the system configuration layer entirely and proceed with remaining layers (user, project, environment), maintaining security without crashing the application.

On Windows and other non-Unix platforms, the symlink_target_is_user_writable function immediately returns false, effectively allowing symlinks in system configuration paths. This design recognizes that the Unix-specific privilege escalation attack (involving UID and permission bits) does not apply to Windows filesystem semantics, though the bounded file size check still applies universally.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →