Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Harden sign-in input bounds and private response caching

25343521418945b250da2fb29d27cffe34ee2a6e · Keenan Nemetz · 3d ago · parent b492b557

Harden sign-in input bounds and private response caching

Bound the account name in LoginBegin before it reaches the per-account rate
limiter or the ceremony store, and bound the passkey label and the credential
fields copied from an authenticator. Rate-limit keys are capped by hashing a
long value, forwarding headers used as addresses are capped, and expired limiter
entries are swept as the table grows.

Mark downloads and rendered pages private unless the repository is readable
anonymously. A private repository's artifact, archive, run artifact, raw file,
and HTML pages previously either carried a public cache directive or no
directive at all, so a shared cache could serve them to an anonymous reader.

Refs #209 (01M6S78Y7EQY596Y9TQRSNRVSE).
Refs #210 (01M6S7936QAYM9TMSCXCYP2CF9).

forge/identity/passkey.go +39

@@ -36,6 +36,20 @@ const (
36 36 DefaultInviteTTL = 72 * time.Hour
37 37
38 38 inviteTokenBytes = 32
39
40 // MaxCredentialLabel bounds the name a person gives a passkey. The label is
41 // shown on the account page and stored beside the credential, so it is a
42 // label, not prose.
43 MaxCredentialLabel = 128
44
45 // Credential fields come from the authenticator and a client, so each is
46 // bounded before it reaches a ceremony or the roster. A value that is not a
47 // passkey shape is refused rather than stored at whatever size the client
48 // chose.
49 maxCredentialTransports = 16
50 maxCredentialTransport = 32
51 maxCredentialAttachment = 32
52 maxCredentialAttestation = 64
39 53 )
40 54
41 55 var (
@@ -394,6 +408,9 @@ func hashToken(token string) string {
394 408 }
395 409
396 410 func validateCredential(c Credential) error {
411 if err := ValidateCredentialLabel(c.Label); err != nil {
412 return err
413 }
397 414 switch {
398 415 case len(c.ID) == 0:
399 416 return errors.New("identity: passkey id is empty")
@@ -403,9 +420,31 @@ func validateCredential(c Credential) error {
403 420 return errors.New("identity: passkey public key is empty")
404 421 case len(c.PublicKey) > 8192:
405 422 return errors.New("identity: passkey public key is too long")
423 case len(c.AttestationType) > maxCredentialAttestation:
424 return fmt.Errorf("identity: passkey attestation type is too long")
425 case len(c.Transport) > maxCredentialTransports:
426 return fmt.Errorf("identity: passkey has %d transports, limit %d", len(c.Transport), maxCredentialTransports)
427 case len(c.Authenticator.Attachment) > maxCredentialAttachment:
428 return fmt.Errorf("identity: passkey attachment is too long")
406 429 case len(c.Authenticator.AAGUID) > 256:
407 430 return errors.New("identity: passkey AAGUID is too long")
408 431 }
432 for _, transport := range c.Transport {
433 if len(transport) > maxCredentialTransport {
434 return fmt.Errorf("identity: passkey transport is %d bytes, limit %d", len(transport), maxCredentialTransport)
435 }
436 }
437 return nil
438 }
439
440 // ValidateCredentialLabel checks the label before a ceremony stores it. A label
441 // is display text, not prose, so it is capped where it is accepted rather than
442 // only when the finished credential is written.
443 func ValidateCredentialLabel(label string) error {
444 label = strings.TrimSpace(label)
445 if len(label) > MaxCredentialLabel {
446 return fmt.Errorf("identity: passkey label is %d bytes, limit %d", len(label), MaxCredentialLabel)
447 }
409 448 return nil
410 449 }
411 450