PHP is_executable()函数

is_executable()函数用于检查指定的文件是否可执行。这在脚本需要判断用户是否有权限执行某个文件时非常有用。

语法

bool is_executable ( string $filename )

参数说明

参数 描述
filename 要检查的文件路径。可以是相对路径或绝对路径

返回值

  • 如果文件存在且可执行,返回true
  • 如果文件不存在或不可执行,返回false
  • 在失败时也返回false(如权限不足无法访问)

文件权限基础

权限位 八进制值 描述 对is_executable()的影响
--- 0 无任何权限 返回false
--x 1 仅执行权限 可能返回true
-w- 2 仅写入权限 返回false
-wx 3 写入和执行权限 可能返回true
r-- 4 仅读取权限 返回false
r-x 5 读取和执行权限 可能返回true
rw- 6 读取和写入权限 返回false
rwx 7 读取、写入和执行权限 可能返回true

注意:实际返回值还取决于当前运行PHP进程的用户权限和文件系统类型

示例代码

示例1:基本文件可执行性检查

<?php
// 检查常见文件类型的可执行性
$files = [
    '/usr/bin/php'      => 'PHP解释器',
    '/bin/ls'          => 'ls命令',
    '/etc/passwd'      => '系统密码文件',
    'test.sh'          => 'shell脚本',
    'index.php'        => 'PHP脚本',
    'nonexistent.txt'  => '不存在的文件'
];

// 创建测试脚本
file_put_contents('test.sh', '#!/bin/bash' . PHP_EOL . 'echo "Hello World"');
chmod('test.sh', 0755); // 设置可执行权限

file_put_contents('test.php', '<?php echo "Hello PHP"; ?>');
chmod('test.php', 0644); // 只设置读写权限

foreach ($files as $file => $description) {
    // 跳过不存在的文件(除了我们特别测试的)
    if ($file === 'nonexistent.txt') {
        $result = is_executable($file) ? '可执行' : '不可执行或不存在';
        echo "$description ($file): $result<br>";
        continue;
    }

    if (file_exists($file)) {
        $executable = is_executable($file);
        $permissions = substr(sprintf('%o', fileperms($file)), -4);

        echo "$description ($file): ";
        echo $executable ? '✓ 可执行' : '✗ 不可执行';
        echo " (权限: $permissions)<br>";
    } else {
        echo "$description ($file): 文件不存在<br>";
    }
}

// 清理测试文件
unlink('test.sh');
unlink('test.php');
?>

示例2:安全命令执行器

<?php
class SafeCommandExecutor {
    private $allowedCommands = [];
    private $allowedPaths = [];

    public function __construct() {
        // 默认允许的系统路径
        $this->allowedPaths = [
            '/usr/bin/',
            '/bin/',
            '/usr/local/bin/'
        ];
    }

    /**
     * 添加允许的命令
     */
    public function allowCommand($command) {
        $this->allowedCommands[] = $command;
        return $this;
    }

    /**
     * 添加允许的路径
     */
    public function allowPath($path) {
        $this->allowedPaths[] = rtrim($path, '/') . '/';
        return $this;
    }

    /**
     * 安全执行命令
     */
    public function execute($command, $args = []) {
        // 1. 检查命令是否在允许列表中
        if (!empty($this->allowedCommands) && !in_array($command, $this->allowedCommands)) {
            throw new Exception("命令 '$command' 不在允许列表中");
        }

        // 2. 查找命令的完整路径
        $fullPath = $this->findCommand($command);
        if (!$fullPath) {
            throw new Exception("未找到命令: $command");
        }

        // 3. 检查文件可执行性
        if (!$this->isSafeExecutable($fullPath)) {
            throw new Exception("命令 '$fullPath' 不可执行或不安全");
        }

        // 4. 执行命令
        $escapedArgs = array_map('escapeshellarg', $args);
        $cmd = escapeshellcmd($fullPath) . ' ' . implode(' ', $escapedArgs);

        $output = [];
        $returnVar = 0;
        exec($cmd, $output, $returnVar);

        return [
            'output' => $output,
            'return_code' => $returnVar,
            'command' => $cmd
        ];
    }

    /**
     * 查找命令的完整路径
     */
    private function findCommand($command) {
        // 检查是否是绝对路径
        if (strpos($command, '/') === 0) {
            return file_exists($command) ? $command : false;
        }

        // 在允许的路径中查找
        foreach ($this->allowedPaths as $path) {
            $fullPath = $path . $command;
            if (file_exists($fullPath)) {
                return $fullPath;
            }
        }

        return false;
    }

    /**
     * 安全检查文件是否可执行
     */
    private function isSafeExecutable($filepath) {
        // 检查文件是否存在
        if (!file_exists($filepath)) {
            return false;
        }

        // 检查是否是普通文件(不是目录或特殊文件)
        if (!is_file($filepath)) {
            return false;
        }

        // 检查文件权限 - 必须是可执行文件
        if (!is_executable($filepath)) {
            return false;
        }

        // 检查文件路径是否在允许的路径中
        foreach ($this->allowedPaths as $allowedPath) {
            if (strpos($filepath, $allowedPath) === 0) {
                return true;
            }
        }

        return false;
    }

    /**
     * 获取文件详细信息
     */
    public function getFileInfo($filepath) {
        if (!file_exists($filepath)) {
            return ['error' => '文件不存在'];
        }

        return [
            'path' => $filepath,
            'realpath' => realpath($filepath),
            'is_executable' => is_executable($filepath),
            'is_file' => is_file($filepath),
            'is_dir' => is_dir($filepath),
            'is_link' => is_link($filepath),
            'permissions' => substr(sprintf('%o', fileperms($filepath)), -4),
            'owner' => fileowner($filepath),
            'group' => filegroup($filepath),
            'size' => filesize($filepath)
        ];
    }
}

// 使用示例
try {
    $executor = new SafeCommandExecutor();

    // 配置允许的路径
    $executor
        ->allowPath('/bin')
        ->allowPath('/usr/bin');

    echo "<h5>安全命令执行示例:</h5>";

    // 检查命令的可执行性
    $commands = ['ls', 'php', 'python', 'nonexistent'];

    foreach ($commands as $cmd) {
        echo "检查命令: $cmd<br>";

        try {
            // 尝试查找命令
            $executor->execute('echo', ['测试']);

            $info = $executor->getFileInfo('/bin/' . $cmd);
            if (isset($info['is_executable']) && $info['is_executable']) {
                echo "  ✓ $cmd 可执行 (权限: {$info['permissions']})<br>";
            } else {
                echo "  ✗ $cmd 不可执行<br>";
            }
        } catch (Exception $e) {
            echo "  ✗ $cmd 错误: " . $e->getMessage() . "<br>";
        }
    }

    echo "<br>";

    // 执行一个安全命令
    try {
        $result = $executor->execute('ls', ['-la', '/tmp']);
        echo "命令执行成功<br>";
        echo "返回码: {$result['return_code']}<br>";
        echo "输出行数: " . count($result['output']) . "<br>";
    } catch (Exception $e) {
        echo "命令执行失败: " . $e->getMessage() . "<br>";
    }

} catch (Exception $e) {
    echo "错误: " . $e->getMessage();
}
?>

示例3:Web应用文件上传安全检查

<?php
class UploadSecurityChecker {
    private $uploadDir;
    private $allowedMimeTypes = [
        'image/jpeg',
        'image/png',
        'image/gif',
        'application/pdf',
        'text/plain'
    ];

    public function __construct($uploadDir = 'uploads') {
        $this->uploadDir = rtrim($uploadDir, '/') . '/';

        // 确保上传目录存在
        if (!is_dir($this->uploadDir)) {
            mkdir($this->uploadDir, 0755, true);
        }
    }

    /**
     * 检查上传的文件
     */
    public function checkUploadedFile($fileField) {
        if (!isset($_FILES[$fileField])) {
            return ['error' => '没有文件上传'];
        }

        $file = $_FILES[$fileField];

        // 基本检查
        if ($file['error'] !== UPLOAD_ERR_OK) {
            return ['error' => $this->getUploadError($file['error'])];
        }

        // 安全检查
        $securityCheck = $this->performSecurityChecks($file);
        if ($securityCheck['safe'] !== true) {
            return ['error' => $securityCheck['reason']];
        }

        // 生成安全文件名
        $safeFilename = $this->generateSafeFilename($file['name']);
        $destination = $this->uploadDir . $safeFilename;

        // 移动文件
        if (move_uploaded_file($file['tmp_name'], $destination)) {
            // 设置安全权限(移除执行权限)
            chmod($destination, 0644);

            return [
                'success' => true,
                'filename' => $safeFilename,
                'path' => $destination,
                'size' => $file['size'],
                'mime_type' => $file['type']
            ];
        }

        return ['error' => '文件移动失败'];
    }

    /**
     * 执行安全检查
     */
    private function performSecurityChecks($file) {
        // 1. 检查MIME类型
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mimeType = finfo_file($finfo, $file['tmp_name']);
        finfo_close($finfo);

        if (!in_array($mimeType, $this->allowedMimeTypes)) {
            return ['safe' => false, 'reason' => '不允许的文件类型: ' . $mimeType];
        }

        // 2. 检查文件扩展名
        $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
        $dangerousExtensions = ['php', 'php3', 'php4', 'php5', 'phtml', 'phar'];
        if (in_array($extension, $dangerousExtensions)) {
            return ['safe' => false, 'reason' => '危险的文件扩展名: ' . $extension];
        }

        // 3. 检查文件是否可执行(上传的临时文件)
        if (is_executable($file['tmp_name'])) {
            return ['safe' => false, 'reason' => '上传的文件具有可执行权限'];
        }

        // 4. 检查文件大小
        $maxSize = 5 * 1024 * 1024; // 5MB
        if ($file['size'] > $maxSize) {
            return ['safe' => false, 'reason' => '文件大小超过限制'];
        }

        // 5. 检查文件内容(简单检查)
        $content = file_get_contents($file['tmp_name'], false, null, 0, 100);
        if (strpos($content, '<?php') !== false) {
            return ['safe' => false, 'reason' => '文件可能包含PHP代码'];
        }

        return ['safe' => true];
    }

    /**
     * 检查上传目录中的文件安全性
     */
    public function checkUploadDirectory() {
        $results = [
            'safe' => [],
            'dangerous' => []
        ];

        $files = glob($this->uploadDir . '*');

        foreach ($files as $file) {
            $info = [
                'filename' => basename($file),
                'path' => $file,
                'is_executable' => is_executable($file),
                'permissions' => substr(sprintf('%o', fileperms($file)), -4),
                'size' => filesize($file)
            ];

            // 检查安全性
            if ($this->isFileSafe($file)) {
                $results['safe'][] = $info;
            } else {
                $results['dangerous'][] = $info;
            }
        }

        return $results;
    }

    /**
     * 检查单个文件是否安全
     */
    private function isFileSafe($filepath) {
        // 检查文件是否可执行
        if (is_executable($filepath)) {
            return false;
        }

        // 检查文件扩展名
        $extension = strtolower(pathinfo($filepath, PATHINFO_EXTENSION));
        $dangerousExtensions = ['php', 'php3', 'php4', 'php5', 'phtml', 'phar', 'exe', 'sh', 'bat'];
        if (in_array($extension, $dangerousExtensions)) {
            return false;
        }

        // 检查文件权限(不应有执行权限)
        $permissions = fileperms($filepath);
        if ($permissions & 0o111) { // 检查执行位
            return false;
        }

        return true;
    }

    /**
     * 生成安全的文件名
     */
    private function generateSafeFilename($originalName) {
        $extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
        $safeExtension = in_array($extension, ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'txt']) ? $extension : 'dat';

        return uniqid('upload_', true) . '.' . $safeExtension;
    }

    /**
     * 获取上传错误信息
     */
    private function getUploadError($errorCode) {
        $errors = [
            UPLOAD_ERR_INI_SIZE => '文件大小超过服务器限制',
            UPLOAD_ERR_FORM_SIZE => '文件大小超过表单限制',
            UPLOAD_ERR_PARTIAL => '文件只有部分被上传',
            UPLOAD_ERR_NO_FILE => '没有文件被上传',
            UPLOAD_ERR_NO_TMP_DIR => '找不到临时文件夹',
            UPLOAD_ERR_CANT_WRITE => '文件写入失败',
            UPLOAD_ERR_EXTENSION => 'PHP扩展阻止了文件上传'
        ];

        return $errors[$errorCode] ?? '未知上传错误';
    }
}

// 使用示例
echo "<h5>文件上传安全检查示例:</h5>";

$checker = new UploadSecurityChecker('test_uploads');

// 检查上传目录
echo "<h6>检查上传目录:</h6>";
$dirCheck = $checker->checkUploadDirectory();
echo "安全文件数: " . count($dirCheck['safe']) . "<br>";
echo "危险文件数: " . count($dirCheck['dangerous']) . "<br>";

// 显示危险文件(如果有)
if (!empty($dirCheck['dangerous'])) {
    echo "<h6>危险文件列表:</h6>";
    foreach ($dirCheck['dangerous'] as $file) {
        echo "- {$file['filename']} (权限: {$file['permissions']}, 可执行: " .
             ($file['is_executable'] ? '是' : '否') . ")<br>";
    }
}

// 模拟上传表单
echo '<form method="post" enctype="multipart/form-data">';
echo '<div class="mb-3">';
echo '<label for="file" class="form-label">选择文件上传测试:</label>';
echo '<input type="file" class="form-control" id="file" name="uploaded_file">';
echo '</div>';
echo '<button type="submit" class="btn btn-primary">上传检查</button>';
echo '</form>';

// 处理上传(演示目的)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['uploaded_file'])) {
    echo "<h6>上传结果:</h6>";
    $result = $checker->checkUploadedFile('uploaded_file');

    if (isset($result['error'])) {
        echo "<div class='alert alert-danger'>上传失败: {$result['error']}</div>";
    } else {
        echo "<div class='alert alert-success'>";
        echo "上传成功!<br>";
        echo "文件名: {$result['filename']}<br>";
        echo "文件大小: {$result['size']} 字节<br>";
        echo "MIME类型: {$result['mime_type']}<br>";
        echo "</div>";
    }
}

// 清理测试目录
if (is_dir('test_uploads')) {
    $files = glob('test_uploads/*');
    foreach ($files as $file) {
        unlink($file);
    }
    rmdir('test_uploads');
}
?>

注意事项

重要提示:
  • 权限缓存:PHP会缓存is_executable()的结果,使用clearstatcache()清除缓存
  • 安全模式:在安全模式下,is_executable()可能受到额外限制
  • Windows系统:在Windows上,is_executable()基于文件扩展名判断(.exe, .bat, .cmd等)
  • 用户权限:检查的是当前运行PHP进程的用户是否有执行权限,不是文件所有者
  • 符号链接:如果文件是符号链接,is_executable()会检查链接指向的文件
  • SELinux/AppArmor:即使文件有执行权限,安全模块也可能阻止执行
  • 文件系统类型:某些文件系统(如FAT)不支持Unix权限,is_executable()可能始终返回true
  • 性能影响:频繁调用is_executable()可能影响性能,特别是在远程文件系统上
  • 目录权限:目录也需要执行权限才能进入,但is_executable()不检查目录

相关函数

  • is_file() - 检查文件是否是普通文件
  • is_dir() - 检查文件是否是目录
  • is_link() - 检查文件是否是符号链接
  • is_readable() - 检查文件是否可读
  • is_writable() - 检查文件是否可写
  • fileperms() - 获取文件权限
  • chmod() - 改变文件模式
  • clearstatcache() - 清除文件状态缓存
  • exec() - 执行外部程序
  • shell_exec() - 通过shell执行命令

典型应用场景

安全命令执行

在执行外部命令前检查命令文件是否可执行,防止命令注入攻击。

文件上传安全

检查上传的文件是否具有可执行权限,防止恶意文件上传。

系统工具检测

检查系统中是否安装了特定工具或程序。

权限管理系统

在文件管理系统中检查用户对文件的执行权限。