|
64 | 64 |
|
65 | 65 | //Make a determiniation if the incoming operation is right for this system. |
66 | 66 | if($gitSystem && $HOSTNAME && $OPERATION){ |
67 | | - $refs=explode("/",$req['ref']); |
| 67 | + // The ref is interpolated into shell commands further down (`git checkout |
| 68 | + // <ref>`) and this endpoint is unauthenticated, so accept only a plain, |
| 69 | + // well-formed ref name and refuse anything else. Without this, a crafted |
| 70 | + // payload could smuggle shell metacharacters through to shell_exec(). |
| 71 | + $incoming_ref = isset($req['ref']) ? trim($req['ref']) : ""; |
| 72 | + if(!preg_match('#^refs/(heads|tags)/[A-Za-z0-9._/-]+$#', $incoming_ref)){ |
| 73 | + _log("Refusing to act on a malformed or unsupported ref: " . json_encode($incoming_ref) . "\n"); |
| 74 | + exit; |
| 75 | + } |
| 76 | + |
| 77 | + $refs=explode("/",$incoming_ref); |
68 | 78 | $incomingOp=$refs[1]; // should be tags or heads |
69 | 79 |
|
70 | 80 | //Some initial validation, make sure this is a configured repo and stuff |
|
86 | 96 | } |
87 | 97 |
|
88 | 98 | //Make sure we're doing a TAG or BRANCH as appropriate |
89 | | - if(($incomingOp == "tags" && $OPERATION = "TAG") || ($incomingOp == "heads" && $refs[2] == $OPERATION)){ |
90 | | - //possibly need to clone it or pull it first: |
91 | | - |
92 | | - $out = do_clone_or_pull(DEPLOY_KEY,$url,$deploy_to, $OPERATION); |
93 | | - _debug("$out\n"); |
94 | | - |
95 | | - //The following will ensure we're either on the right branch, or the right tag: |
96 | | - $out = do_checkout_branch($req,$deploy_to, $OPERATION); |
97 | | - _debug("$out\n"); |
98 | | - |
| 99 | + // NOTE: the first clause below is `==`, not `=`. It was previously an |
| 100 | + // assignment, which made the condition true for EVERY tag push regardless of |
| 101 | + // how this host is configured -- and, worse, reassigned $OPERATION to "TAG" |
| 102 | + // for the rest of the request. The effect was that a tag push drove a |
| 103 | + // branch-tracking host (our test server) down the tag code path, leaving its |
| 104 | + // checkout detached at that tag. |
| 105 | + if(($incomingOp == "tags" && $OPERATION == "TAG") || ($incomingOp == "heads" && $refs[2] == $OPERATION)){ |
| 106 | + |
| 107 | + // Order matters, and it differs between the two cases: |
| 108 | + // |
| 109 | + // - TAG: fetch first. The tag usually doesn't exist locally yet, so |
| 110 | + // checking it out before fetching would fail. |
| 111 | + // - BRANCH: check out the branch first. `git pull` aborts outright |
| 112 | + // from a detached HEAD ("You are not currently on a branch"), and |
| 113 | + // the old fetch-then-checkout order could never recover from that: |
| 114 | + // the pull failed, the local branch stayed behind, and the deploy |
| 115 | + // ran anyway (see below) -- so the container rebuilt from stale |
| 116 | + // source while looking freshly deployed. |
| 117 | + $repo_exists = is_dir($deploy_to) && file_exists($deploy_to . "/.git"); |
| 118 | + |
| 119 | + if($OPERATION != "TAG" && $repo_exists){ |
| 120 | + $out = do_checkout_branch($req,$deploy_to, $OPERATION); |
| 121 | + _debug("$out\n"); |
| 122 | + |
| 123 | + $out = do_clone_or_pull(DEPLOY_KEY,$url,$deploy_to, $OPERATION); |
| 124 | + _debug("$out\n"); |
| 125 | + } |
| 126 | + else{ |
| 127 | + //possibly need to clone it or pull it first: |
| 128 | + $out = do_clone_or_pull(DEPLOY_KEY,$url,$deploy_to, $OPERATION); |
| 129 | + _debug("$out\n"); |
| 130 | + |
| 131 | + //The following will ensure we're either on the right branch, or the right tag: |
| 132 | + $out = do_checkout_branch($req,$deploy_to, $OPERATION); |
| 133 | + _debug("$out\n"); |
| 134 | + } |
| 135 | + |
| 136 | + // Guard against silently deploying stale source. Historically the |
| 137 | + // container build below ran unconditionally after checkout/pull, so a |
| 138 | + // failed pull (detached HEAD, merge conflict, network/ssh error) would |
| 139 | + // rebuild from STALE source while looking freshly deployed. Confirm the |
| 140 | + // checked-out HEAD is the commit that was actually pushed ($req['after']) |
| 141 | + // before building. If the payload carries no usable SHA we fall back to |
| 142 | + // deploying (preserves prior behavior rather than risk blocking a deploy). |
| 143 | + // (On rapid successive pushes HEAD may already be a *newer* commit than |
| 144 | + // this event's 'after'; skipping here is safe -- the newer push's hook |
| 145 | + // deploys the newer state.) |
| 146 | + // |
| 147 | + // IMPORTANT: 'after' is the ref's new VALUE, which for an ANNOTATED tag is |
| 148 | + // the tag OBJECT's sha -- not the commit's. `git checkout refs/tags/X` |
| 149 | + // leaves HEAD at the COMMIT, so a raw sha comparison would abort every |
| 150 | + // annotated-tag deploy, i.e. every production release (2.27.0/.2/.3 are all |
| 151 | + // annotated). Peel with `^{commit}` before comparing. |
| 152 | + $expected_sha = isset($req['after']) ? strtolower(trim($req['after'])) : ""; |
| 153 | + $has_sha = preg_match('/^[0-9a-f]{40}$/', $expected_sha) && !preg_match('/^0+$/', $expected_sha); |
| 154 | + |
| 155 | + $head_sha = ""; |
| 156 | + $expected_commit = ""; |
| 157 | + if($has_sha){ |
| 158 | + $head_sha = strtolower(trim(shell_exec("bash -c 'cd $deploy_to; git rev-parse HEAD' 2>/dev/null"))); |
| 159 | + $expected_commit = strtolower(trim(shell_exec( |
| 160 | + "bash -c 'cd $deploy_to; git rev-parse --verify --quiet {$expected_sha}^{commit}' 2>/dev/null"))); |
| 161 | + } |
| 162 | + |
| 163 | + // Fail SAFE: abort only when we positively know HEAD is the wrong commit. |
| 164 | + // A sha we can't resolve locally (fetch failed, unusual payload, ...) falls |
| 165 | + // through to the historical deploy-anyway behavior rather than blocking a |
| 166 | + // release on a check we aren't sure about. |
| 167 | + if($head_sha !== "" && $expected_commit !== "" && $head_sha !== $expected_commit){ |
| 168 | + _log("ABORTING DEPLOY: checked-out HEAD ($head_sha) does not match the ". |
| 169 | + "pushed commit ($expected_commit, from ref value $expected_sha) -- ". |
| 170 | + "the pull/checkout above likely ". |
| 171 | + "failed. Refusing to rebuild the container from stale source.\n"); |
| 172 | + exit; |
| 173 | + } |
| 174 | + |
99 | 175 | //If we're using docker, then do someother stuff |
100 | 176 | if($USE_DOCKER){ |
101 | 177 |
|
@@ -195,7 +271,7 @@ function do_checkout_branch($req, $deployto, $operation){ |
195 | 271 | */ |
196 | 272 | function determine_branch_name( $request) { |
197 | 273 | // push request, branch is in request[ref] |
198 | | - if (isset($reqest['ref'])) { |
| 274 | + if (isset($request['ref'])) { |
199 | 275 | // strip out the refs/head nonsense -- doesn't look like bare |
200 | 276 | // branch is listed anywhere in the request |
201 | 277 | return preg_replace("|refs/heads/|", "", $request['ref']); |
@@ -280,18 +356,24 @@ function do_clone($key,$url,$path) { |
280 | 356 | * Log a message, only here to make some messages easy to turn off. |
281 | 357 | */ |
282 | 358 | function _trace($message) { |
283 | | - if ($trace = TRUE) { |
| 359 | + // NOTE: was `if ($trace = TRUE)` -- an assignment, not a comparison, so it |
| 360 | + // was always true and could never be turned off (same =/== footgun fixed in |
| 361 | + // the deploy condition above). Default to on to preserve prior behavior; |
| 362 | + // config.php can silence it with `define('AUTODEPLOY_TRACE', false);`. |
| 363 | + if (defined('AUTODEPLOY_TRACE') ? AUTODEPLOY_TRACE : TRUE) { |
284 | 364 | _log($message); |
285 | | - } |
| 365 | + } |
286 | 366 | } |
287 | 367 |
|
288 | 368 | /** |
289 | 369 | * Log a message, only here to make some messages easy to turn off. |
290 | 370 | */ |
291 | 371 | function _debug($message) { |
292 | | - if ($debug = TRUE) { |
| 372 | + // Same fix as _trace(): `$debug = TRUE` was an assignment, always true. |
| 373 | + // Default on; silence via `define('AUTODEPLOY_DEBUG', false);` in config.php. |
| 374 | + if (defined('AUTODEPLOY_DEBUG') ? AUTODEPLOY_DEBUG : TRUE) { |
293 | 375 | _log($message); |
294 | | - } |
| 376 | + } |
295 | 377 | } |
296 | 378 |
|
297 | 379 | /** |
|
0 commit comments