From c5c530668817d57dfdce5f8d93c21f3da2501742 Mon Sep 17 00:00:00 2001 From: Catherine Date: Thu, 23 Apr 2026 15:16:48 +0000 Subject: [PATCH] [breaking-change] Use a distinct scope for forge DNS allowlist authz. Before this commit, a `_git-pages-repository.` TXT record would allow both forge DNS allowlist authorization, as well as normal DNS allowlist authorization. This means that a site set up to have its contents updated by a Forgejo Action could have its contents replaced by the contents of the repository which contains the Forgejo Action, which will effectively erase the site in most cases. This is a classic confused deputy scenario. To fix this, forge DNS allowlist authorization now uses a distinct `_git-pages-forge-allowlist.` TXT record, removing ambiguity that allows this scenario to happen. The issue was introduced in 27a6de792c3c1d94cfa1d6eb40bcc06773496e97 and existed in `main` for about a hour, so it is unlikely anybody has been impacted by this. --- README.md | 2 +- src/auth.go | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 649aa82..43339a9 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ The authorization flow for content updates (`PUT`, `PATCH`, `DELETE`, `POST` req - **Index repository:** If the request URL is `scheme://./`, a *matching* clone URL is computed by templating `[[wildcard]].clone-url` with `` and ``, where `` is computed by templating each element of `[[wildcard]].index-repos` with ``, and `[[wildcard]]` is the section where the match occurred. - **Project repository:** If the request URL is `scheme://.//`, a *matching* clone URL is computed by templating `[[wildcard]].clone-url` with `` and ``, and `[[wildcard]]` is the section where the match occurred. 5. **Forge Authorization (wildcard):** If the method is `PUT` or `PATCH` or `DELETE`, and (unless the method is `DELETE`) the body contains an archive, and a `[[wildcard]]` configuration section exists where the suffix of a hostname (compared label-wise) is equal to `[[wildcard]].domain`, and `[[wildcard]].authorization` is non-empty, and the request includes a `Forge-Authorization:` header, and the header (when forwarded as `Authorization:`) grants push permissions to a repository at the *matching* clone URL (as defined above) as determined by an API call to the forge, the request is authorized. -6. **Forge Authorization (DNS allowlist):** If the method is `PUT` or `PATCH` or `DELETE`, and (unless the method is `DELETE`) the body contains an archive, and the request URL is `scheme://./`, and a TXT record lookup at `_git-pages-repository.` returns a set of well-formed absolute URLs, and the request includes a `Forge-Authorization:` header, and the header (when forwarded as `Authorization:`) grants push permissions to a repository at any of the URLs in the TXT records as determined by an API call to the forge, the request is authorized. +6. **Forge Authorization (DNS allowlist):** If the method is `PUT` or `PATCH` or `DELETE`, and (unless the method is `DELETE`) the body contains an archive, and the request URL is `scheme://./`, and a TXT record lookup at `_git-pages-forge-allowlist.` returns a set of well-formed absolute URLs, and the request includes a `Forge-Authorization:` header, and the header (when forwarded as `Authorization:`) grants push permissions to a repository at any of the URLs in the TXT records as determined by an API call to the forge, the request is authorized. 7. **Default Deny:** Otherwise, the request is not authorized. The authorization flow for metadata retrieval (`GET` requests with site paths starting with `.git-pages/`) in the following order, with the first of multiple applicable rule taking precedence: diff --git a/src/auth.go b/src/auth.go index e69cdfa..067ae31 100644 --- a/src/auth.go +++ b/src/auth.go @@ -179,7 +179,7 @@ func authorizeDNSChallenge(r *http.Request) (*Authorization, error) { }, nil } -func authorizeDNSAllowlist(r *http.Request) (*Authorization, error) { +func authorizeDNSAllowlist(r *http.Request, scope string) (*Authorization, error) { host, err := GetHost(r) if err != nil { return nil, err @@ -190,7 +190,7 @@ func authorizeDNSAllowlist(r *http.Request) (*Authorization, error) { return nil, err } - allowlistHostname := fmt.Sprintf("_git-pages-repository.%s", host) + allowlistHostname := fmt.Sprintf("_%s.%s", scope, host) records, err := net.LookupTXT(allowlistHostname) if err != nil { return nil, AuthError{http.StatusUnauthorized, @@ -421,7 +421,7 @@ func AuthorizeUpdateFromRepository(r *http.Request) (*Authorization, error) { // DNS allowlist gives authority to update but not delete. if r.Method == http.MethodPut || r.Method == http.MethodPost { - auth, err = authorizeDNSAllowlist(r) + auth, err = authorizeDNSAllowlist(r, "git-pages-repository") if err != nil && IsUnauthorized(err) { causes = append(causes, err) } else if err != nil { // bad request @@ -741,7 +741,7 @@ func authorizeForgeWildcard(r *http.Request) (*Authorization, error) { } // Validates a provided forge token against a repository URL extracted from the DNS allowlist -// records of the target domain (`_git-pages-repository.*`). +// records of the target domain specified in `_git-pages-forge-authorization.*`. func authorizeForgeDNSAllowlist(r *http.Request) (*Authorization, error) { forgeToken := r.Header.Get("Forge-Authorization") if forgeToken == "" { @@ -749,7 +749,7 @@ func authorizeForgeDNSAllowlist(r *http.Request) (*Authorization, error) { } var errs []error - if dnsAuth, err := authorizeDNSAllowlist(r); err != nil { + if dnsAuth, err := authorizeDNSAllowlist(r, "git-pages-forge-allowlist"); err != nil { errs = append(errs, err) } else if dnsAuth != nil { // DNS allows uploads from some repositories, but we don't know yet if the forge token