fix: add JWT token expiry and replace shell exec with spawn in git routes#88
Conversation
Zhang-Henry
left a comment
There was a problem hiding this comment.
Code Review
Two independent changes: (1) configurable JWT expiry, and (2) exec → spawn migration in git routes to eliminate shell injection. Both directions are correct and necessary.
Critical — Verify Migration Completeness
The most important thing to confirm before merging: The diff claims all execAsync calls in git.js are converted to spawnAsync, but the full file on the repo shows several execAsync calls still present. This may be a branch sync issue. The following lines must be confirmed as migrated on the PR branch:
| Location | Risk |
|---|---|
GET /commit-diff (~L708-711) |
Shell injection — req.query.commit is a raw query parameter interpolated into a shell string. GET /commit-diff?commit=abc;rm -rf / would execute arbitrary commands. |
GET /commits (~L679-682) |
commit.hash from git log output interpolated into shell string |
validateGitRepository (~L100, ~L107) |
Lower risk but still shell-interpolated |
GET /status (~L277) |
Lower risk |
GET /branches (~L568) |
Lower risk |
Do not merge until all execAsync usages are confirmed gone on the PR branch.
JWT Changes (server/middleware/auth.js)
The expiry addition is correct. Minor notes:
JWT_EXPIRYis not validated at startup — a misconfigured value likeJWT_EXPIRY=invalidwill crash the login endpoint at sign-time, not at startup. Consider adding early validation.- Hardcoded fallback secret (
'claude-ui-dev-secret-change-in-production') pre-dates this PR but is worth noting — tokens signed with this known-public secret are trivially forgeable. Should log a loud warning ifJWT_SECRETis not set in non-dev environments. - The
authenticateTokenmiddleware returns a generic 401 for both expired and invalid tokens. A follow-up could distinguishTokenExpiredErrorto help clients decide whether to re-login vs. show an error.
spawnAsync Implementation
The wrapper is well-structured. Notes:
git diffandgit statusreturn exit code 1 when changes exist (not an error).spawnAsyncrejects on non-zero exit, matching priorexecAsyncbehavior. This is a latent issue worth documenting — not a regression.- No timeout on
spawnAsync— a hung git operation (SSH push waiting for credentials) will block the route handler indefinitely. Same as before but worth a follow-up.
Minor
server/routes/git.js,generateCommitMessageWithAI(~L815-872): Contains 10+console.logcalls with emoji prefixes that are clearly debug logs. Not introduced by this PR, but if the surrounding area is being touched, it's a good time to clean up.- If the migration is complete, confirm the
execimport fromchild_processandpromisifyfromutilare removed.
1b6b8ac to
a111d4f
Compare
|
@Zhang-Henry Thank you for the review! Updates pushed: exec → spawn migrationAll Scope note: JWT improvements
|
…utes JWT tokens: - Add configurable expiry (default 7 days) via JWT_EXPIRY env var - Previously tokens never expired, meaning a leaked token grants permanent access until the user record is deleted Git routes: - Replace all execAsync (shell=true) calls with spawnAsync (shell=false) - execAsync passes commands through the system shell, which could interpret metacharacters in paths or commit hashes - spawnAsync passes arguments as an array, avoiding shell interpretation - Remove unused exec/promisify imports Affected call sites in git.js: - git rev-parse --is-inside-work-tree - git rev-parse --show-toplevel - git status --porcelain - git branch -a - git show --stat (commit hash was interpolated into shell string) - git show <commit> (commit hash was interpolated into shell string)
- Validate JWT_EXPIRY at startup by attempting a test sign — crashes immediately on misconfiguration instead of at first login attempt - Warn loudly when JWT_SECRET is not set in production environment
Reduce friction for users who may forget credentials with shorter expiry. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
a111d4f to
844363e
Compare
Summary
Two security hardening improvements.
1. JWT tokens now expire (default: 7 days)
Previously,
generateToken()issued tokens with noexpiresInoption — once issued, a token was valid forever. A leaked token (e.g., from browser localStorage, logs, or a shared machine) would grant permanent access.Now tokens expire after 7 days by default. Configurable via
JWT_EXPIRYenvironment variable (accepts any ms-compatible duration string, e.g.,"24h","30d").2. Git routes:
execAsync(shell=true) →spawnAsync(shell=false)6 call sites in
server/routes/git.jsusedexecAsync()which internally useschild_process.execwithshell: true. Two of these interpolated user-influenced values (commit hashes) directly into shell command strings:Replaced all 6 with
spawnAsync()which passes arguments as an array, avoiding shell interpretation:Removed the now-unused
execandpromisifyimports.Test plan
JWT_EXPIRY=1h→ verify token expires after 1 hour