sudo执行脚本为什么不会加载全部的环境变量

安全机制设计背景

sudo 的设计目标是允许用户以其他身份(如 root)执行命令,同时 最小化安全风险。环境变量可能被恶意用户篡改(例如通过 LD_PRELOADPATH 等变量注入恶意代码),因此 sudo 默认会 清理环境变量,仅保留少数安全相关的变量

源码分析

  • 使用sudo-1.8这个版本的代码来分析

https://github.com/sudo-project/sudo.git

// src/sudo.c
int
main(int argc, char *argv[], char *envp[])
{
    int nargc, ok, status = 0;
    char **nargv, **env_add;
    char **user_info, **command_info, **argv_out, **user_env_out;
    const char * const allowed_prognames[] = { "sudo", "sudoedit", NULL };
    struct sudo_settings *settings;
    struct plugin_container *plugin, *next;
    sigset_t mask;
    debug_decl_vars(main, SUDO_DEBUG_MAIN)
......
	case MODE_RUN:
	    ok = policy_check(&policy_plugin, nargc, nargv, env_add,
		&command_info, &argv_out, &user_env_out);
......
  • main函数中,argc是参数量,argv[]是参数向量,envp[]是当前进程的环境变量,由os载入。环境变量相关的核心变量是user_env_out,在MODE_RUN(sudo 运行标识)类型下,通过policy_check方法,进行相关权限校验和环境变量的改写等。
// src/sudo.c
static int
policy_check(struct plugin_container *plugin, int argc, char * const argv[],
    char *env_add[], char **command_info[], char **argv_out[],
    char **user_env_out[])
{
    int ret;
    debug_decl(policy_check, SUDO_DEBUG_PCOMM)

    if (plugin->u.policy->check_policy == NULL) {
	sudo_fatalx(U_("policy plugin %s is missing the `check_policy' method"),
	    plugin->name);
    }
    sudo_debug_set_active_instance(plugin->debug_instance);
    ret = plugin->u.policy->check_policy(argc, argv, env_add, command_info,
	argv_out, user_env_out);
    sudo_debug_set_active_instance(sudo_debug_instance);
    debug_return_int(ret);
}
  • 在这个方法中,核心调用是plugin->u.policy->check_policy,这个方法是通过sudo_load_plugin方法载入到虚函数表中,然后调用的,所以用普通的IDE是没法识别的,这个方法可以用gdb调试等方法,最后确定是调用到了sudoers_policy_main这个函数
// plugins/sudoers/sudoers.c
int
sudoers_policy_main(int argc, char * const argv[], int pwflag, char *env_add[],
    bool verbose, void *closure)
{
    char **edit_argv = NULL;
    char *iolog_path = NULL;
    mode_t cmnd_umask = ACCESSPERMS;
    struct sudo_nss *nss;
    int cmnd_status = -1, oldlocale, validated;
    int ret = -1;
    debug_decl(sudoers_policy_main, SUDOERS_DEBUG_PLUGIN)
......
    /* Build a new environment that avoids any nasty bits. */
    if (!rebuild_env())
	goto bad;
......
}
// plugins/sudoers/env.c
bool
rebuild_env(void)
{
    char **ep, *cp, *ps1;
    char idbuf[MAX_UID_T_LEN + 1];
    unsigned int didvar;
    bool reset_home = false;
    debug_decl(rebuild_env, SUDOERS_DEBUG_ENV)

    /*
     * Either clean out the environment or reset to a safe default.
     */
    ps1 = NULL;
    didvar = 0;
    env.env_len = 0;
    env.env_size = 128;
    free(env.old_envp);
    env.old_envp = env.envp;
......
    if (def_env_reset || ISSET(sudo_mode, MODE_LOGIN_SHELL)) {
	/*
	 * If starting with a fresh environment, initialize it based on
	 * /etc/environment or login.conf.  For "sudo -i" we want those
	 * variables to override the invoking user's environment, so we
	 * defer reading them until later.
	 */
	if (!ISSET(sudo_mode, MODE_LOGIN_SHELL)) {
#ifdef HAVE_LOGIN_CAP_H
	    /* Insert login class environment variables. */
	    if (login_class) {
		login_cap_t *lc = login_getclass(login_class);
		if (lc != NULL) {
		    setusercontext(lc, runas_pw, runas_pw->pw_uid,
			LOGIN_SETPATH|LOGIN_SETENV);
		    login_close(lc);
		}
	    }
#endif /* HAVE_LOGIN_CAP_H */
#if defined(_AIX) || (defined(__linux__) && !defined(HAVE_PAM))
	    /* Insert system-wide environment variables. */
	    if (!read_env_file(_PATH_ENVIRONMENT, true, false))
		sudo_warn("%s", _PATH_ENVIRONMENT);
#endif
	    for (ep = env.envp; *ep; ep++)
		env_update_didvar(*ep, &didvar);
	}

	/* Pull in vars we want to keep from the old environment. */
	for (ep = env.old_envp; *ep; ep++) {
	    bool keepit;

	    /*
	     * Look up the variable in the env_check and env_keep lists.
	     */
	    keepit = env_should_keep(*ep);

	    /*
	     * Do SUDO_PS1 -> PS1 conversion.
	     * This must happen *after* env_should_keep() is called.
	     */
	    if (strncmp(*ep, "SUDO_PS1=", 9) == 0)
		ps1 = *ep + 5;

	    if (keepit) {
		/* Preserve variable. */
		CHECK_PUTENV(*ep, true, false);
		env_update_didvar(*ep, &didvar);
	    }
	}
	didvar |= didvar << 16;		/* convert DID_* to KEPT_* */

	/*
	 * Add in defaults.  In -i mode these come from the runas user,
	 * otherwise they may be from the user's environment (depends
	 * on sudoers options).
	 */
	if (ISSET(sudo_mode, MODE_LOGIN_SHELL)) {
	    CHECK_SETENV2("SHELL", runas_pw->pw_shell,
		ISSET(didvar, DID_SHELL), true);
#ifdef _AIX
	    CHECK_SETENV2("LOGIN", runas_pw->pw_name,
		ISSET(didvar, DID_LOGIN), true);
#endif
	    CHECK_SETENV2("LOGNAME", runas_pw->pw_name,
		ISSET(didvar, DID_LOGNAME), true);
	    CHECK_SETENV2("USER", runas_pw->pw_name,
		ISSET(didvar, DID_USER), true);
	} else {
	    /* We will set LOGNAME later in the def_set_logname case. */
	    if (!def_set_logname) {
#ifdef _AIX
		if (!ISSET(didvar, DID_LOGIN))
		    CHECK_SETENV2("LOGIN", user_name, false, true);
#endif
		if (!ISSET(didvar, DID_LOGNAME))
		    CHECK_SETENV2("LOGNAME", user_name, false, true);
		if (!ISSET(didvar, DID_USER))
		    CHECK_SETENV2("USER", user_name, false, true);
	    }
	}

	/* If we didn't keep HOME, reset it based on target user. */
	if (!ISSET(didvar, KEPT_HOME))
	    reset_home = true;

	/*
	 * Set MAIL to target user in -i mode or if MAIL is not preserved
	 * from user's environment.
	 */
	if (ISSET(sudo_mode, MODE_LOGIN_SHELL) || !ISSET(didvar, KEPT_MAIL)) {
	    if (_PATH_MAILDIR[sizeof(_PATH_MAILDIR) - 2] == '/') {
		if (asprintf(&cp, "MAIL=%s%s", _PATH_MAILDIR, runas_pw->pw_name) == -1)
		    goto bad;
	    } else {
		if (asprintf(&cp, "MAIL=%s/%s", _PATH_MAILDIR, runas_pw->pw_name) == -1)
		    goto bad;
	    }
	    if (sudo_putenv(cp, ISSET(didvar, DID_MAIL), true) == -1) {
		free(cp);
		goto bad;
	    }
	    sudoers_gc_add(GC_PTR, cp);
	}
    } else {
	/*
	 * Copy environ entries as long as they don't match env_delete or
	 * env_check.
	 */
	for (ep = env.old_envp; *ep; ep++) {
	    /* Add variable unless it matches a black list. */
	    if (!env_should_delete(*ep)) {
		if (strncmp(*ep, "SUDO_PS1=", 9) == 0)
		    ps1 = *ep + 5;
		else if (strncmp(*ep, "SHELL=", 6) == 0)
		    SET(didvar, DID_SHELL);
		else if (strncmp(*ep, "PATH=", 5) == 0)
		    SET(didvar, DID_PATH);
		else if (strncmp(*ep, "TERM=", 5) == 0)
		    SET(didvar, DID_TERM);
		CHECK_PUTENV(*ep, true, false);
	    }
	}
......
}

环境变量覆盖的总体逻辑

环境重置模式下的覆盖流程

  • 强制初始化一个干净的环境,忽略用户传递的大部分变量。
  • 通过 /etc/environmentlogin.conf 或目标用户(runas_pw)的配置重新设置变量。
  • 覆盖的优先级:系统默认值(/etc/environment) < 用户传递的环境变量 < sudo 强制覆盖的变量(如 PATHHOME

覆盖的触发条件

  1. 环境重置模式(def_env_reset || MODE_LOGIN_SHELL):
if (!ISSET(sudo_mode, MODE_LOGIN_SHELL)) {
    // 1. 从 login.conf 加载变量(BSD 系统)
    setusercontext(lc, runas_pw, runas_pw->pw_uid, LOGIN_SETPATH|LOGIN_SETENV);

    // 2. 从 /etc/environment 加载变量(Linux/AIX)
    read_env_file(_PATH_ENVIRONMENT, true, false);

    // 3. 标记已处理的变量(更新 didvar)
    for (ep = env.envp; *ep; ep++)
        env_update_didvar(*ep, &didvar);
}
// 作用:用系统级的默认值初始化环境(如安全的 PATH)。
// 关键点:此时 didvar 会记录已设置的变量(如 PATH),后续用户传递的同名变量可能被忽略。
// 选择性保留用户变量
for (ep = env.old_envp; *ep; ep++) {
    // 检查变量是否在 env_keep 白名单中
    bool keepit = env_should_keep(*ep);

    if (keepit) {
        CHECK_PUTENV(*ep, true, false);  // 保留用户变量
        env_update_didvar(*ep, &didvar); // 标记为已处理
    }
}
// 规则:
// 若变量在白名单(如 TERM、DISPLAY),则保留用户传递的值。
// 否则被系统默认值覆盖。
// 如果用户未提供 SHELL,则设置为目标用户的 shell
if (!ISSET(didvar, DID_SHELL))
    CHECK_SETENV2("SHELL", runas_pw->pw_shell, false, true);

// 如果未保留 HOME,则重置为目标用户的家目录
if (!ISSET(didvar, KEPT_HOME))
    reset_home = true;

非重置模式下的覆盖流程

  • 保留用户传递的环境变量,但过滤掉黑名单(env_delete)中的变量。
for (ep = env.old_envp; *ep; ep++) {
    // 过滤黑名单变量(env_delete)
    if (!env_should_delete(*ep)) {
        // 特殊处理 SUDO_PS1 -> PS1
        if (strncmp(*ep, "SUDO_PS1=", 9) == 0)
            ps1 = *ep + 5;

        // 记录已处理的变量(PATH/SHELL/TERM)
        else if (strncmp(*ep, "SHELL=", 6) == 0)
            SET(didvar, DID_SHELL);
        else if (strncmp(*ep, "PATH=", 5) == 0)
            SET(didvar, DID_PATH);

        // 保留非黑名单变量
        CHECK_PUTENV(*ep, true, false);
    }
}
// 规则:
// 直接保留用户传递的变量,除非在 env_delete 黑名单中。
// 仍会标记 PATH/SHELL 等变量为已处理(didvar),但不会强制覆盖。

总之,用sudo执行脚本等操作的时候,默认会重置大部分环境变量,只保留允许的变量,在某些限制场景下,使用需要注意。如果允许,可以加参数-E来载入所有环境变量,但这种方式受到/etc/sudoersenv_keep 配置的限制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Clarence Liu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值