Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions pkg/provider/github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,17 @@ func (v *Provider) SetClient(ctx context.Context, run *params.Run, event *info.E
// If Global and Repo level configurations are not provided then lets not override the provider token.
if token != "" {
event.Provider.Token = token
} else if len(v.RepositoryIDs) > 0 {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The scoping logic here relies on v.RepositoryIDs being populated, but this slice is currently only populated for Pull Request events (within populateRunEventFromPullRequest). For other event types, such as Push, v.RepositoryIDs remains empty. Consequently, the else if block is skipped, and the token remains unscoped (having access to all repositories in the installation). This defeats the security objective of the PR for non-PR event types.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 641eadc

// We need to keep the token unscoped until ScopeTokenToListOfRepos so that CreateToken can
// look up the extra repos from the configmap.
// Token is scoped to only the calling repo if no additional scoping repos are configured
// so that no unwanted remote tasks are executed.
ns := info.GetNS(ctx)
scopedToken, err := v.GetAppToken(ctx, run.Clients.Kube, event.Provider.URL, event.InstallationID, ns)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There is an inconsistency in the URL passed to GetAppToken. This call uses event.Provider.URL (the API URL, e.g., https://api.github.com/ or https://ghe.example.com/api/v3), whereas the initial token generation in InitAppClient (line 191) uses event.GHEURL (the base URL). For GitHub Enterprise installations, this discrepancy may lead to authentication failures or incorrect API endpoint resolution depending on how the underlying GetAppToken implementation handles the host/URL argument.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gemini-code-assist event.GHEURL and event.Provider.URL values are set to be same in

  • buildEventFromPipelineRun
  • ParsePayload

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the clarification. While the values are currently set to be the same in those specific functions, using event.Provider.URL in GetAppToken introduces a dependency on the provider's API URL for token generation, whereas event.GHEURL is explicitly intended for base URL resolution. If event.Provider.URL is ever modified or configured differently (e.g., for specific API sub-paths), this could lead to authentication issues. Using event.GHEURL is safer and more consistent with the existing implementation in InitAppClient.

if err != nil {
return fmt.Errorf("failed to scope token to triggering repository: %w", err)
}
event.Provider.Token = scopedToken
}
}

Expand Down
28 changes: 23 additions & 5 deletions pkg/provider/github/parse_payload.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,18 +131,29 @@ type Payload struct {
Installation struct {
ID *int64 `json:"id"`
} `json:"installation"`
Repository struct {
ID *int64 `json:"id"`
} `json:"repository"`
}

func getInstallationIDFromPayload(payload string) (int64, error) {
func getInstallationAndRepoIDFromPayload(payload string) (int64, int64, error) {
var data Payload
err := json.Unmarshal([]byte(payload), &data)
if err != nil {
return -1, err
return -1, -1, err
}

var installationID int64 = -1
var repoID int64 = -1
if data.Installation.ID != nil {
return *data.Installation.ID, nil
installationID = *data.Installation.ID
}

if data.Repository.ID != nil {
repoID = *data.Repository.ID
}
return -1, nil

return installationID, repoID, nil
}

// ParsePayload will parse the payload and return the event
Expand Down Expand Up @@ -177,7 +188,7 @@ func (v *Provider) ParsePayload(ctx context.Context, run *params.Run, request *h
return nil, err
}

installationIDFrompayload, err := getInstallationIDFromPayload(payload)
installationIDFrompayload, repoIDFromPayload, err := getInstallationAndRepoIDFromPayload(payload)
if err != nil {
return nil, err
}
Expand All @@ -189,6 +200,10 @@ func (v *Provider) ParsePayload(ctx context.Context, run *params.Run, request *h
}
}

if repoIDFromPayload > 0 {
v.RepositoryIDs = []int64{repoIDFromPayload}
}

eventInt, err := github.ParseWebHook(event.EventType, []byte(payload))
if err != nil {
return nil, err
Expand Down Expand Up @@ -537,6 +552,7 @@ func (v *Provider) handleReRequestEvent(ctx context.Context, event *github.Check
// fine because you can't do a rereq without being a github owner?
runevent.Sender = event.GetSender().GetLogin()
v.userType = event.GetSender().GetType()
v.RepositoryIDs = []int64{event.GetRepo().GetID()}
return runevent, nil
}
runevent.PullRequestNumber = event.GetCheckRun().GetCheckSuite().PullRequests[0].GetNumber()
Expand Down Expand Up @@ -584,6 +600,7 @@ func (v *Provider) handleCheckSuites(ctx context.Context, event *github.CheckSui
// fine because you can't do a rereq without being a github owner?
runevent.Sender = event.GetSender().GetLogin()
v.userType = event.GetSender().GetType()
v.RepositoryIDs = []int64{event.GetRepo().GetID()}
return runevent, nil
}
runevent.PullRequestNumber = event.GetCheckSuite().PullRequests[0].GetNumber()
Expand Down Expand Up @@ -699,6 +716,7 @@ func (v *Provider) handleCommitCommentEvent(ctx context.Context, event *github.C
runevent.BaseURL = runevent.HeadURL
runevent.TriggerTarget = triggertype.Push
v.userType = event.GetSender().GetType()
v.RepositoryIDs = []int64{event.GetRepo().GetID()}

repo, err := MatchEventURLRepo(ctx, v.Run, runevent, "")
if err != nil {
Expand Down
8 changes: 4 additions & 4 deletions pkg/resolve/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ func resolveRemoteResources(ctx context.Context, rt *matcher.RemoteTasks, types
// making sure that the pipeline with same annotation name is not fetched
if alreadyFetchedResource(fetchedResourcesForEvent.Pipelines, remotePipeline) {
rt.Logger.Debugf("skipping already fetched pipeline %s in annotations on pipelinerun %s", remotePipeline, pipelinerun.GetName())
// already fetched, then just get the pipeline to add to run specific Resources
pipeline = fetchedResourcesForEvent.Pipelines[remotePipeline]
// already fetched, deep-copy so inlining for this run doesn't mutate the cached original
pipeline = fetchedResourcesForEvent.Pipelines[remotePipeline].DeepCopy()
} else {
// seems like a new pipeline, fetch it based on name in annotation
pipeline, err = rt.GetPipelineFromAnnotationName(ctx, remotePipeline)
Expand Down Expand Up @@ -181,7 +181,7 @@ func resolveRemoteResources(ctx context.Context, rt *matcher.RemoteTasks, types
// if task is already fetched in the event, then just copy the task
if alreadyFetchedResource(fetchedResourcesForEvent.Tasks, remoteTask) {
rt.Logger.Debugf("skipping already fetched task %s in annotations on pipelinerun %s", remoteTask, pipelinerun.GetName())
task = fetchedResourcesForEvent.Tasks[remoteTask]
task = fetchedResourcesForEvent.Tasks[remoteTask].DeepCopy()
} else {
// get the task from annotation name
task, err = rt.GetTaskFromAnnotationName(ctx, remoteTask)
Expand Down Expand Up @@ -213,7 +213,7 @@ func resolveRemoteResources(ctx context.Context, rt *matcher.RemoteTasks, types

// if PipelineRef is used then, first resolve pipeline and replace all taskRef{Finally/Task} of Pipeline, then put inlinePipeline in PipelineRun
if pipelinerun.Spec.PipelineRef != nil && pipelinerun.Spec.PipelineRef.Resolver == "" {
pipelineResolved := fetchedResourcesForPipelineRun.Pipeline
pipelineResolved := fetchedResourcesForPipelineRun.Pipeline.DeepCopy()
turns, err := inlineTasks(pipelineResolved.Spec.Tasks, ropt, fetchedResourcesForPipelineRun)
if err != nil {
return nil, err
Expand Down
78 changes: 78 additions & 0 deletions pkg/resolve/remote_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,84 @@ func TestRemote(t *testing.T) {
}
}

// Verifies that cached remote pipelines are deep-copied before modification.
// Without deep copy, when the first PipelineRun applies its task annotation,
// it would mutate the cached pipeline and leak that task into the second run.
func TestSharedRemotePipelineCacheNotMutated(t *testing.T) {
taskName := "shared-task"

pipelineTask := tektonv1.TaskSpec{
Steps: []tektonv1.Step{
{Name: "from-pipeline", Image: "alpine", Command: []string{"true"}},
},
}
pipelinerunTask := tektonv1.TaskSpec{
Steps: []tektonv1.Step{
{Name: "from-pipelinerun", Image: "busybox", Command: []string{"false"}},
},
}

// remote pipeline with a single TaskRef
pipeline := ttkn.MakePipeline("shared-pipeline", []tektonv1.PipelineTask{
{Name: taskName, TaskRef: &tektonv1.TaskRef{Name: taskName}},
}, map[string]string{
apipac.Task: "http://remote/shared-task",
})
pipelineB, err := yaml.Marshal(pipeline)
assert.NilError(t, err)

pipelineTaskB, err := ttkn.MakeTaskB(taskName, pipelineTask)
assert.NilError(t, err)
pipelinerunTaskB, err := ttkn.MakeTaskB(taskName, pipelinerunTask)
assert.NilError(t, err)

remoteURLS := map[string]map[string]string{
"http://remote/shared-pipeline": {"body": string(pipelineB), "code": "200"},
"http://remote/shared-task": {"body": string(pipelineTaskB), "code": "200"},
"http://remote/pr-task": {"body": string(pipelinerunTaskB), "code": "200"},
}

// first run: overrides the task via pipelinerun annotation
// second run: same pipeline, no override β€” should get the original task
pipelineruns := []*tektonv1.PipelineRun{
ttkn.MakePR("first-run", map[string]string{
apipac.Pipeline: "http://remote/shared-pipeline",
apipac.Task: "http://remote/pr-task",
}, tektonv1.PipelineRunSpec{
PipelineRef: &tektonv1.PipelineRef{Name: "shared-pipeline"},
}),
ttkn.MakePR("second-run", map[string]string{
apipac.Pipeline: "http://remote/shared-pipeline",
}, tektonv1.PipelineRunSpec{
PipelineRef: &tektonv1.PipelineRef{Name: "shared-pipeline"},
}),
}

observer, _ := zapobserver.New(zap.InfoLevel)
logger := zap.New(observer).Sugar()
ctx, _ := rtesting.SetupFakeContext(t)
httpTestClient := httptesthelper.MakeHTTPTestClient(remoteURLS)
rt := &matcher.RemoteTasks{
ProviderInterface: &testprovider.TestProviderImp{},
Logger: logger,
Run: &params.Run{
Clients: clients.Clients{HTTP: *httpTestClient},
},
}

ret, err := resolveRemoteResources(ctx, rt, TektonTypes{PipelineRuns: pipelineruns}, &Opts{RemoteTasks: true, GenerateName: true})
assert.NilError(t, err)
assert.Equal(t, len(ret), 2)

firstStep := ret[0].Spec.PipelineSpec.Tasks[0].TaskSpec.Steps[0]
assert.Equal(t, firstStep.Name, "from-pipelinerun")
assert.Equal(t, firstStep.Image, "busybox")

secondStep := ret[1].Spec.PipelineSpec.Tasks[0].TaskSpec.Steps[0]
assert.Equal(t, secondStep.Name, "from-pipeline")
assert.Equal(t, secondStep.Image, "alpine")
}

func TestAssembleTaskFQDNs(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading