GitHub CLI 新增短效存取權杖,但更新權杖仍可能遭惡意軟件讀取
GitHub CLI's New Expiring Tokens Still Leave Your Refresh Token Exposed to Malware
GitHub CLI 新增可自動更新的短效存取權杖,但更新權杖存於作業系統鑰匙圈,macOS 上同一用戶權限下的程序可在沒有確認提示的情況下讀取。存取權杖 8 小時後失效,更新權杖則可在閒置六個月後才到期;新功能目前仍是公開預覽及選擇加入。文中比較的 ghtkn 會以密碼衍生的金鑰加密儲存更新權杖。
GitHub CLI (gh) recently added short-lived access tokens that are refreshed automatically. It is a public preview and opt-in for now, but it is planned to become the default.
https://github.blog/changelog/2026-10-21-github-cli-opt-in-to-expiring-auto-refreshing-oauth-tokens/
This is a welcome change: a leaked access token now stops working within eight hours.
But the refresh token is a different story.
It lives for months, and gh stores it in the OS keyring, where on macOS any process running as you can read it without a confirmation dialog, as gh's own documentation acknowledges.
If malware steals the refresh token, it can keep minting new access tokens off your machine.
I develop ghtkn, a CLI that creates short-lived GitHub App user access tokens for local development, and it was designed with exactly this threat in mind.
This article compares ghtkn with gh's new short-lived tokens and explains what ghtkn protects against that gh doesn't.
How ghtkn refreshes tokens
Like gh, ghtkn issues access tokens that expire in eight hours and refreshes them automatically with a refresh token, so you only go through the device flow once per app.
The difference is where the tokens live.
ghtkn supports refresh tokens only on its agent backend: a local daemon that stores tokens encrypted with a key derived from your passphrase.
The agent starts locked, and tokens become available only after you unlock it with your passphrase.
For details, see the documentation:
https://github.com/suzuki-shunsuke/ghtkn/blob/main/docs/refresh-token.md
How gh and ghtkn compare
Here is where ghtkn differs from gh's short-lived tokens.
- The refresh token is hard to steal even if arbitrary code runs on your machine
- You can restrict which organizations and repositories a token can access
- You can use different apps with different permissions for different purposes
- The refresh token is never stored in plain text
- You can lock the agent so that no token can be obtained
- A suspected refresh token leak is detected and reported
- Go CLIs integrate seamlessly through the SDK
Let's look at each of them in detail.
The refresh token is hard to steal even if arbitrary code runs on your machine
Protecting the refresh token is essential to keeping short-lived tokens meaningful.
First, note that access tokens can be obtained with either tool.
gh gives you one with gh auth token, and ghtkn gives you one with ghtkn get, so there is no difference in the fact that an access token can leak if arbitrary code runs on your machine.
However, an access token expires in eight hours, but a refresh token doesn't.
According to GitHub's documentation, a refresh token expires only after six months without use, and each refresh issues a new refresh token.
So if a refresh token leaks, an attacker can keep refreshing it and mint access tokens off your machine practically forever.
That largely defeats the purpose of making access tokens short-lived.
gh stores the refresh token in the OS keyring by default, but if arbitrary code runs on your machine, for example through a supply chain attack, secrets stored in the OS keyring can easily leak.
On macOS, gh accesses the Keychain through /usr/bin/security, and once /usr/bin/security is trusted (most people have trusted it), it can read and write secrets without any confirmation dialog.
gh's own document about the macOS keyring mentions this as well.
Moreover, the name of gh's Keychain item is obvious from the source code, so malware can easily get the refresh token.
When I actually tried it, a single security command gave me both the access token and the refresh token without any confirmation dialog.
ghtkn's agent backend, on the other hand, encrypts and stores the refresh token on its own.
The encryption key is derived from the user's passphrase, and the passphrase is never saved to disk.
The agent also has no API that returns the refresh token; clients can only obtain access tokens.
Furthermore, a non-root process running as the same user can't read the agent's memory.
macOS restricts reading other processes' memory by default, and on Linux the agent marks its own process non-dumpable (PR_SET_DUMPABLE=0) at startup.
So even if arbitrary code runs on your machine, the refresh token doesn't easily leak unless the attacker escalates to root.
You can restrict which organizations and repositories a token can access
gh's token can access every repository the user can access, and there is no way to restrict it.
Also, gh is a privileged app, so organizations can't restrict its access with their settings.
ghtkn, on the other hand, uses GitHub App user access tokens, so a token can only access the organizations and repositories the app is installed on.
A private app can only be installed on the organization or user that owns it, so even if the access token of your personal GitHub App leaks, it can't access your company's organization.
You can also restrict a token to specific repositories by selecting them when you install the app.
You can use different apps with different permissions for different purposes
gh is a single app, so you can't switch between apps, and it tends to have strong permissions.
It requires at least the repo, read:org, and gist scopes, so you can't even make it read-only.
With ghtkn, on the other hand, you can register multiple GitHub Apps and switch between them depending on what you are doing.
For example, you can use an app with weak permissions most of the time, and a different app only when you need strong permissions.
In addition, the agent backend automatically removes the access tokens and refresh tokens of apps that have gone unused for a certain period.
So if you keep a separate app with strong permissions that you only use occasionally, its tokens are removed naturally while you aren't using it, which reduces the chance of a leak.
The refresh token is never stored in plain text
gh stores the access token and the refresh token in the OS keyring, but if writing to the OS keyring fails, it stores them in plain text in hosts.yml.
It prints only a one-line warning and the login succeeds anyway, so users are unlikely to notice.
On headless Linux or in containers without an OS keyring, this is in fact the behavior you are likely to get.
ghtkn, on the other hand, never stores access tokens in plain text unless you explicitly select the text backend.
And since refresh tokens are supported only on the agent backend, a refresh token is never stored in plain text.
You can lock the agent so that no token can be obtained
With gh, any process running as you can obtain a token at any time, for example with gh auth token.
gh has no mechanism that lets users make tokens temporarily unobtainable.
ghtkn's agent has the same exposure while it is unlocked: any process running as the same user can obtain access tokens from it.
That's why ghtkn lets you lock the agent with ghtkn agent lock.
While the agent is locked, no token can be obtained at all, including access tokens, and you have to enter your passphrase to unlock it before using it again.
Locking doesn't need the passphrase, so you can hook it into a screen lock or logout.
By keeping the agent locked while you don't need tokens for a while, you shorten the window in which an attacker can obtain them.
A suspected refresh token leak is detected and reported
A refresh token is replaced with a new one every time it is used (rotation).
So if an attacker uses a stolen refresh token first, the legitimate user's next refresh fails.
This can be a signal that the refresh token has leaked.
When a refresh fails, gh deletes the credential and only prints the token for <host> has expired; please run 'gh auth login' to re-authenticate.
Nothing indicates a possible leak, and since the credential is deleted, nothing is left to investigate.
Users will think the token simply expired and log in again without suspecting anything.
ghtkn's agent, on the other hand, returns a warning about a possible leak to the client, which shows it to the user, when a refresh token that should still be valid fails to refresh.
This lets users notice the leak and start investigating and responding to it.
Go CLIs integrate seamlessly through the SDK
ghtkn provides a Go SDK, and various tools such as aqua, pinact, and ghir obtain access tokens seamlessly through it.
You don't need to expose access tokens as environment variables, which reduces the paths through which they can leak.
Will gh be able to do the same in the future?
ghtkn supports refresh tokens only on the agent backend, so that refresh tokens don't easily leak even if arbitrary code runs in your development environment.
gh has no such architecture with a constantly running daemon, at least for now.
Also, because gh is a single privileged app, it would be hard for it to let you switch between apps to restrict permissions and access scope.
I look forward to future improvements in gh, but I believe ghtkn will keep its advantages on these points for the time being.
Conclusion
Short-lived tokens becoming officially supported in gh is great news, but they leave the refresh token exposed, and ghtkn still has clear security advantages over them.
I will keep developing ghtkn.
If you got interested in ghtkn, please give it a try:
https://github.com/suzuki-shunsuke/ghtkn#rocket-getting-started
The content of this article is also available in ghtkn's documentation.
With ghtkn v0.4.2 or later, you can read the documentation with the ghtkn docs command.
ghtkn is AI-friendly, so if you have any questions, ask a coding agent such as Claude Code.
To let your agent read ghtkn's documentation, install ghtkn's agent skill:
https://github.com/suzuki-shunsuke/ghtkn#installing-agent-skills
When I installed the skill and asked Claude Code, "GitHub CLI apparently supports short-lived tokens now. Does that make ghtkn unnecessary?", it explained the advantages based on the documentation.
For how ghtkn is made AI-friendly, see the following article:
https://dev.to/suzukishunsuke/developing-ai-friendly-clis-10m
來源:DEV Community · dev.to