Repository navigation
Hybrid: harden linking, sign-ins, the relay, names and same-site requests - #309
Merged
Merged
Conversation
…ests
A security review of the hybrid self-host features, with a regression test
for each fix:
- Brokered sign-ins (Notion, Confluence, Google Drive) are finished in a
holding partition and become the install's only when it redeems the code
with its secret and PKCE verifier. Starting one is rate limited.
- Linking CoderHelm's GitHub App needs a confirmation on a page that names
the install's address and can't be framed; only installations of
CoderHelm's own app count; a link reaches only the repositories the person
who linked it can push to (sent to GitHub as the token's repository list);
return addresses are compared host by host.
- Requests to an install's address (relay forwards, ownership proofs) never
follow redirects and read at most 16 KB of the answer.
- The API refuses changes sent with cookies from another origin on the same
site (customer {name}.coderhelm.com pages share a site with app and api).
- Names that read as CoderHelm's own (coderhelm with digit look-alikes, or a
part such as app, login, billing or support) are reserved.
- The Route 53 permissions also deny anything under CoderHelm's own hosts,
mail and certificate-authority records, and names three labels deep.
- Relayed events also sign the source and event headers (v2); the intake
refuses an event when it can't record its delivery id.
- Revoking an install removes its GitHub links and keeps its CloudFront
address from registering again.
- docs/self-host/security.md gains the hybrid threat model.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A security review of the hybrid self-host features (registration, the event relay, the token and sign-in brokers, CoderHelm addresses and DNS, the public endpoints, deliveries). This PR fixes what it found, each with a regression test, and adds the hybrid threat model to the security docs. Details of the findings went to the maintainers directly.
Changes
routes/hybrid.rs,mcp_remote/runtime.rs): a Notion, Confluence or Google Drive sign-in started through the hosted broker is finished in a holding partition and moves into the install's partition only when the install redeems the code at the token endpoint with its secret and PKCE verifier (Runtime::adopt_sign_in, resealed under the install's KMS context). The public authorize endpoint is rate limited per address.routes/hybrid.rs,hybrid/broker.rs): after the GitHub sign-in, a confirmation page (no-store,frame-ancestors 'none') names the install's address; onlyPOST /hybrid/github/confirmwith its one-time token links. Only installations of CoderHelm's own app count. A link stores a ceiling, the repositories the linker can push to (fromGET /user/installations/{id}/repositories), and every token is minted with at most those repositories, so GitHub enforces it.set_link_reposrefuses repositories outside the ceiling. Return addresses are compared host by host. Links made before this keep their behaviour until linked again.hybrid/relay.rs): relay forwards and ownership proofs use a client that never follows redirects and read at most 16 KB of an answer.middleware/cors.rs): a change (not GET, HEAD or OPTIONS) sent with cookies from a same-site origin that isn't a trusted dashboard origin is refused with 403 (Sec-Fetch-Site, and on the hosted side the origin for browsers without it).hybrid/names.rs): names containingcoderhelm(digit look-alikes included) or a part such asapp,api,login,billingorsupportare reserved. Names already claimed are unchanged.infra/lib/hybrid.ts): a second deny statement for anything under CoderHelm's own hosts,_domainkey,_dmarc,_mta-sts,_smtp,_spf,_acme-challenge, and names three labels deep (IAM's*crosses dots, so the allow alone matched them).hybrid/sign.rs,routes/hybrid_install.rs): the relay header addsv2, which also covers the source and event headers; installs check it when present and still acceptv1alone from an older hosted side. The intake answers 503 (and the relay retries) when it can't record a delivery id, instead of handling the event.routes/hybrid.rs): revoking an install also removes its GitHub links and keeps its CloudFront address from registering again; linking checks the install is active.docs/self-host/security.mdgains a hybrid threat model (assets, actors, each attack and its control, what CoderHelm can and cannot see, known limits).docs/self-host/hybrid.mdis corrected: addresses under coderhelm.com are the same site as app and api to a browser.Follow-ups outside this repo: the dashboard and the docs install guide don't yet have words for the new link outcomes (
no_write_access,declined,install_off).Verification
cargo fmt --check,cargo clippy --all-targets -- -D warnings,cargo test(workspace).the_relay_never_follows_a_redirect,a_large_error_answer_is_cut,a_customer_subdomain_cant_send_changes_with_the_session,without_sec_fetch_site_the_hosted_side_goes_by_the_origin,look_alike_names_are_refused,the_route_of_a_relayed_event_is_signed,the_intake_never_takes_a_copy_it_couldnt_record,a_sign_in_is_adopted_from_a_holding_partition,a_provider_sign_in_is_held_apart_from_the_install,a_link_never_reaches_past_what_its_linker_can_push_to,writable_repositories_come_from_the_persons_own_permissions,the_return_address_is_one_of_the_installs_own_hosts,the_link_confirmation_names_the_install_and_cant_be_framed,a_revoked_cloudfront_is_one_key_however_it_is_written, and the updatedlinking_needs_the_person_to_see_the_installation.infra:npm test, with the Route 53 test extended to check the deeper names are fenced and the install's own records stay writable; the generated managed policies stay under IAM's size limit.