Skip to content

GitHub App installation token is written to build logs when an app uses private submodules #5618

Open

Description

@cpuchip

What happens

When an application's source is the GitHub provider with Enable Submodules on, and the repository's .gitmodules uses relative URLs (url = ../other-repo.git, the usual way to make private submodules clone), every build log contains the GitHub App installation token in clear text:

Submodule 'third_party/example' (https://oauth2:ghs_…@github.com/<owner>/example.git) registered for path …

One line per submodule, every build.

Why

packages/server/src/utils/providers/github.ts puts the token into the clone URL (v0.29.8 line 167; still on main at line 232 as of 2026-10-08):

const cloneUrl = `${cloneBase.protocol}//oauth2:${token}@${repoclone}`;

and clones with --recurse-submodules. Git resolves each relative submodule URL against that remote URL, so the submodules inherit the token (which is what makes private submodules work at all), and git prints each resolved URL as it registers the submodule.

Impact

  • The logs live under /etc/dokploy/logs/<app>/ (mode 644 on my install) and are shown in the Dokploy UI, including the live stream during a build.
  • An installation token is short-lived, about an hour, but while it lives it carries the installation's access: every repository the GitHub App is installed on, not only the one being built.

Related: #4600 covers other secrets reaching the logs through shell interpolation. This one is the GitHub token, reaching the log through git's own output rather than through a command line, so a command-level redaction would not catch it.

Suggested fix (not tested here)

Keep the token out of the URL so nothing derived from it is ever printed. One way is a header for the clone and everything it spawns:

git -c http.extraHeader="Authorization: Basic $(printf 'x-access-token:%s' "$TOKEN" | base64 -w0)" \
    clone --recurse-submodules https://github.com/<owner>/<repo>.git …

(-c settings reach the submodule clones through GIT_CONFIG_PARAMETERS.) A credential helper or GIT_ASKPASS would work as well. As defence in depth, the deployment log writer could redact //oauth2:[^@]+@.

Workaround in use meanwhile

A root timer that redacts https://oauth2:<token>@ in finished build logs, touching only files idle for a couple of minutes, because rewriting a log that is still being written truncates it.

Environment

dokploy/dokploy v0.29.8, self-hosted on Ubuntu 24.04 with Docker Swarm; GitHub provider through a GitHub App; application build type Dockerfile with Enable Submodules on. The line is unchanged on main as of 2026-10-08 (latest release v0.30.8).

Activity

  1. linear-code commented on Oct 8, 2026

    @linear-code
  2. Souvik-Cyclic commented on Oct 8, 2026

    @Souvik-Cyclic
    Contributor

    I'd like to work on this. Reproduced it locally: the token reaches the log through the submodule URL that git resolves from the clone URL. Plan is to keep the token out of the URL and pass it as a host-scoped http.<url>.extraHeader (the same Authorization: Basic header git already sends), which also keeps it out of .git/config. Tested with a private relative submodule: clone works and the token no longer appears in the output.

  3. Souvik-Cyclic commented on Oct 8, 2026

    @Souvik-Cyclic
    Contributor

    Opened #5619 to fix this. It also covers GitLab and Gitea, which build the clone URL the same way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions