安全机制设计背景
sudo 的设计目标是允许用户以其他身份(如 root)执行命令,同时 最小化安全风险。环境变量可能被恶意用户篡改(例如通过 LD_PRELOAD
、PATH
等变量注入恶意代码),因此 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/environment
、login.conf
或目标用户(runas_pw
)的配置重新设置变量。 - 覆盖的优先级:系统默认值(
/etc/environment
) < 用户传递的环境变量 <sudo
强制覆盖的变量(如PATH
、HOME
)
覆盖的触发条件
- 环境重置模式(
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/sudoers
中 env_keep
配置的限制。