现在位置: 首页 > Skills 教程 > 正文

Skills 权限与安全控制

Skills 可以访问文件系统、调用外部 API、执行脚本,这些能力如果被滥用会带来安全风险。

本篇介绍如何在设计阶段建立安全边界,防止 Skill 做出超出预期的操作。


Skills 的默认权限范围

在 Claude 的执行环境中,Skills 的权限由沙箱环境决定,并非无限制的。

操作类型权限状态说明
读取上传文件允许仅限 /mnt/user-data/uploads/
写入输出文件允许仅限 /mnt/user-data/outputs/ 和 /home/claude/
读取系统文件受限只读挂载,无法修改系统文件
访问外部网络受限仅允许访问白名单域名
执行任意系统命令受限不能使用 sudo,不能修改系统配置
访问其他用户数据禁止沙箱隔离保证

Skills 在设计上是"最小权限"原则的实践:Skill 只能访问它明确需要的资源。如果一个 Skill 需要超出上述范围的权限,应当重新考虑设计方案。


在 SKILL.md 中明确权限边界

在 Skill 文档中清晰声明它会访问哪些资源,让用户在使用前了解 Skill 的行为范围。

## 权限说明

本 Skill 会进行以下操作,请确认你已了解:

**文件访问**
- 读取:/mnt/user-data/uploads/ 下用户上传的文件
- 写入:/mnt/user-data/outputs/ 下的输出文件

**网络访问**
- 无(本 Skill 不访问任何外部网络)

**不会进行的操作**
- 不读取系统文件
- 不修改已上传的原始文件
- 不访问任何外部服务

防止路径穿越攻击

当脚本接受用户提供的文件路径时,需要验证路径是否在允许范围内,防止用户通过 ../ 访问到不应访问的目录。

实例

# 文件路径:scripts/safe_path.py
import os

# 允许读取的目录
ALLOWED_READ_DIRS = [
    "/mnt/user-data/uploads",
    "/mnt/skills/public",
]

# 允许写入的目录
ALLOWED_WRITE_DIRS = [
    "/mnt/user-data/outputs",
    "/home/claude",
]

def is_safe_path(path: str, allowed_dirs: list) -> bool:
    """
    检查路径是否在允许的目录范围内

    防止 ../../../etc/passwd 类型的路径穿越攻击
    """

    # 解析为绝对路径(消除 .. 和符号链接)
    real_path = os.path.realpath(os.path.abspath(path))

    for allowed in allowed_dirs:
        real_allowed = os.path.realpath(allowed)
        # 检查 real_path 是否以 allowed 目录开头
        if real_path.startswith(real_allowed + os.sep) or real_path == real_allowed:
            return True
    return False

def safe_read_path(user_input: str) -> str:
    """验证读取路径,不合法时抛出异常"""
    if not is_safe_path(user_input, ALLOWED_READ_DIRS):
        raise PermissionError(
            f"拒绝访问:{user_input}\n"
            f"只允许读取以下目录:{ALLOWED_READ_DIRS}"
        )
    return os.path.realpath(user_input)

def safe_write_path(user_input: str) -> str:
    """验证写入路径,不合法时抛出异常"""
    if not is_safe_path(user_input, ALLOWED_WRITE_DIRS):
        raise PermissionError(
            f"拒绝写入:{user_input}\n"
            f"只允许写入以下目录:{ALLOWED_WRITE_DIRS}"
        )
    return os.path.realpath(user_input)

# 使用示例
if __name__ == "__main__":
    # 正常路径:通过
    ok_path = safe_read_path("/mnt/user-data/uploads/runoob.csv")
    print(f"通过:{ok_path}")

    # 穿越路径:拒绝
    try:
        bad_path = safe_read_path("/mnt/user-data/uploads/../../etc/passwd")
    except PermissionError as e:
        print(f"已拦截:{e}")
通过:/mnt/user-data/uploads/runoob.csv
已拦截:拒绝访问:/mnt/user-data/uploads/../../etc/passwd
只允许读取以下目录:['/mnt/user-data/uploads', '/mnt/skills/public']

API 密钥的安全存储

不同的密钥管理方式,安全等级差异显著。

方式安全性推荐程度
硬编码在脚本中极低,会被提交到 Git禁止
环境变量中,进程隔离推荐(开发阶段)
.env 文件(加入 .gitignore)中,文件本地存储推荐(本地使用)
系统密钥管理器(如 Vault)高,集中管理推荐(生产环境)

实例

# 文件路径:scripts/config.py
# 安全地从多个来源读取配置,按优先级降序查找

import os

def get_secret(key: str, required: bool = True) -> str:
    """
    按优先级从以下来源读取密钥:
    1. 环境变量(最高优先级)
    2. /home/claude/.skill_secrets 文件(本地密钥文件)
    3. 若 required=True 且找不到,抛出异常
    """

    # 1. 环境变量
    value = os.environ.get(key)
    if value:
        return value

    # 2. 本地密钥文件(每行格式:KEY=VALUE)
    secrets_file = "/home/claude/.skill_secrets"
    if os.path.exists(secrets_file):
        with open(secrets_file) as f:
            for line in f:
                line = line.strip()
                if line.startswith(f"{key}="):
                    return line[len(key)+1:]

    # 3. 未找到
    if required:
        raise EnvironmentError(
            f"缺少必要的密钥:{key}\n"
            f"请设置环境变量:export {key}='你的密钥'"
        )
    return ""

输入内容的安全过滤

当 Skill 将用户输入传递给 Shell 命令时,必须对输入进行转义,防止命令注入。

实例

# 文件路径:scripts/safe_exec.py
import subprocess
import shlex

def safe_shell_exec(template: str, user_input: str) -> str:
    """
    安全地将用户输入嵌入 Shell 命令

    错误做法:os.system(f"process {user_input}")   # 命令注入风险!
    正确做法:使用参数列表,让 subprocess 处理转义
    """

    # 使用列表而非字符串,subprocess 会自动处理转义
    cmd = ["python", "scripts/process.py", user_input]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
    return result.stdout

# 如果必须构建字符串命令,使用 shlex.quote 转义用户输入
def safe_string_exec(user_filename: str) -> str:
    safe_name = shlex.quote(user_filename)   # 自动添加引号并转义特殊字符
    cmd = f"wc -l {safe_name}"
    result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
    return result.stdout

永远不要用 os.system(f"cmd {user_input}") 这种方式执行命令。用户输入中若包含 ; rm -rf / 之类的内容,会导致灾难性后果。始终使用 subprocess 的列表参数形式。