Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

apiserver: fix cve for CORS #112809

Merged
merged 1 commit into from
Jan 30, 2023
Merged

apiserver: fix cve for CORS #112809

merged 1 commit into from
Jan 30, 2023

Conversation

tkashem
Copy link
Contributor

@tkashem tkashem commented Sep 30, 2022

What type of PR is this?

/kind bug

What this PR does / why we need it:

This makes an attempt to fix CVE-2022-1996
https://huntr.dev/bounties/be837427-415c-4d8c-808b-62ce20aa84f1/

  • validate that regular expression(s) specified in --cors-allowed-origins server run option matches the start and end of the host inside of an origin header. If not, then fail with an error so that the cluster operator can modify the regular expression accordingly.

Which issue(s) this PR fixes:

Fixes #

Special notes for your reviewer:

Does this PR introduce a user-facing change?

kube-apiserver: regular expressions specified with the `--cors-allowed-origins` option are now validated to match the entire `hostname` inside the `Origin` header of the request and 
must contain '^' or the '//' prefix to anchor to the start, and '$' or the port separator ':' to anchor to 
the end.

Additional documentation e.g., KEPs (Kubernetes Enhancement Proposals), usage docs, etc.:


@k8s-ci-robot k8s-ci-robot added release-note-none Denotes a PR that doesn't merit a release note. do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. kind/bug Categorizes issue or PR as related to a bug. size/M Denotes a PR that changes 30-99 lines, ignoring generated files. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. do-not-merge/needs-sig Indicates an issue or PR lacks a `sig/foo` label and requires one. needs-triage Indicates an issue or PR lacks a `triage/foo` label and requires one. needs-priority Indicates a PR lacks a `priority/foo` label and requires one. labels Sep 30, 2022
@tkashem
Copy link
Contributor Author

tkashem commented Sep 30, 2022

/sig api-machinery
/assign @liggitt @aojea

@k8s-ci-robot k8s-ci-robot added sig/api-machinery Categorizes an issue or PR as relevant to SIG API Machinery. and removed do-not-merge/needs-sig Indicates an issue or PR lacks a `sig/foo` label and requires one. labels Sep 30, 2022
@aojea
Copy link
Member

aojea commented Sep 30, 2022

List of allowed origins for CORS, comma separated. An allowed origin can be a regular expression to support subdomain matching. If this list is empty CORS will not be enabled.

--cors-allowed-origins=example.com will mach example.com.not.allowed

then should we imply each regexp has a $ at the end?
I mean --cors-allowed-origins=example.com will bet the same as --cors-allowed-origins=example.com$

@tkashem
Copy link
Contributor Author

tkashem commented Sep 30, 2022

then should we imply each regexp has a $ at the end?

I think this is effectively adding "^" at the beginning and "$" at the end:
https://github.com/kubernetes/kubernetes/pull/112809/files#diff-364c58df33bc37c53167689110a964d071912e415ee5a266e648cc5735ab9b49R81-R84

Instead of using MatchString, it's using FindString and making sure the length of the matched string matches the length of the origin. Maybe there is a better way of doing this.

		if leftMostMatch := re.FindString(origin); len(leftMostMatch) == len(origin) {
			return true
		}

I think an alternative is to use FindStringIndex and make sure the right index from the returned pair is equal to len(origin)

@tkashem tkashem changed the title [WIP] apiserver: fix cve for CORS apiserver: fix cve for CORS Sep 30, 2022
@k8s-ci-robot k8s-ci-robot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Sep 30, 2022
@tkashem tkashem force-pushed the cors-cve branch 2 times, most recently from f946f10 to 373e9df Compare October 1, 2022 01:43
@aojea
Copy link
Member

aojea commented Oct 2, 2022

I think this is effectively adding "^" at the beginning and "$" at the end:

the flag is documented as a list of regular expressions, I think that is on users to define those options on the regexp

#71669

// Dispatch to the next handler
handler.ServeHTTP(w, req)
})
}

func isAllowed(origin string, allowedOriginPatternsREs []*regexp.Regexp) bool {
for _, re := range allowedOriginPatternsREs {
if leftRightIndex := re.FindStringIndex(origin); len(leftRightIndex) == 2 && leftRightIndex[1] == len(origin) {
Copy link
Member

Choose a reason for hiding this comment

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

I'm pretty sure this does not do the right thing when the origin header contains the scheme and/or the port

The :port portion is optional (according to the RFC)

This reminds me a lot of openshift/origin#16204

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks @liggitt for the pointer

I made some changes assuming the following:

  • we want to proactively prevent this CVE even though the regex defined by the cluster operator does not fully match the host name in origin.
  • the kube-apiserver can't change the cluster operator defined regex on the fly, there is no way for us to verify the validity of the changed regex. is it as simple as appending a (:|$) to the regexp string?

Alternatively we could:

  • No changes in CORS implementation, we can ask the cluster operator to fix their regex to match the entirety of the origin header (how about the case when the origin header has multiple origins?)
  • Change the CORS filter to check if the regex matches the entirety of the origin header and report to log or metric that there could be a potential CVE there

Copy link
Member

Choose a reason for hiding this comment

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

we want to proactively prevent this CVE even though the regex defined by the cluster operator does not fully match the host name in origin.

There's multiple categories I can see existing clusters being in:

Unsafe options, for example:

  1. literal domain name not realizing it is a regex (e.g. foo.bar.com, which can match fooxbar.com)
  2. regex for a single origin not realizing they are doing a partial match (e.g. foo\.com, which can match foo.companybar.com)
  3. regex for multiple origins not realizing they are doing a partial match (e.g. (foo\.com|bar\.com), which can match foo.companybar.com)
  4. regex intending to do partial matches not realizing they are also doing partial suffix matches (e.g. .*\.foo\.com, which can match x.foo.companybar.com)

Safe options, for example:

  1. regex which forces matching the whole host (e.g. //foo\.com(:|$))
  2. regex which forces matching the whole origin and scheme (e.g. https://foo\.com(:|$))
  3. regex which pins to the start/end (e.g. ^https://foo\.com(:|$))

We don't want to change anything that would make already safe options stop matching what they already matched. Blindly prepending // and appending (:|$) to the regex would break those. That only worked in the openshift PR I linked because that was taking literal hostnames as inputs, not regexes of full URLs.

Distinguishing intent when someone has unescaped . characters in their config is also tricky. Probably it's a bug and they mean't to match a literal ., so should have written /., but we can't be 100% sure.

I think trying to auto-fix things in the unsafe category to be safe is going to be really hard to do without any possibility of breaking someone's intended behavior. Would it make more sense to detect when the provided regex doesn't do anything that would pin to the start (missing ^ or //) or end (missing : or $) and fail validation of the flag with a message indicating how they can modify the flag value? That would have the benefit of prompting the operator to make their config safe in a way they would also probably want to apply to older versions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@liggitt please review the changes when you have some time:

  • fail validation if the regular expressions in cors-allowed-origins does not pin to start/end
  • run the regular expression on each origin from the header (the origin header may contain multiple origins)

@fedebongio
Copy link
Contributor

/triage accepted

@k8s-ci-robot k8s-ci-robot added triage/accepted Indicates an issue or PR is ready to be actively worked on. and removed needs-triage Indicates an issue or PR lacks a `triage/foo` label and requires one. labels Oct 4, 2022
@tkashem tkashem force-pushed the cors-cve branch 2 times, most recently from 824c72a to f3b3f91 Compare November 3, 2022 22:49
@tkashem
Copy link
Contributor Author

tkashem commented Nov 4, 2022

1m17s
--
{Nov  3 23:10:40.443: timed out while waiting for pod ephemeral-containers-test-2816/ephemeral-containers-target-pod to be container debugger running failed test/e2e/framework/pod/pod_client.go:172 k8s.io/kubernetes/test/e2e/framework/pod.(*PodClient).AddEphemeralContainerSync(0xc000fd06c0, 0xc002860c00, 0xc003a11e10, 0x3?) 	test/e2e/framework/pod/pod_client.go:172 +0x65c k8s.io/kubernetes/test/e2e/common/node.glob..func6.2() 	test/e2e/common/node/ephemeral_containers.go:72 +0x528}

/test pull-kubernetes-conformance-kind-ga-only-parallel

}
for _, re := range allowedOriginPatternsREs {
if re.MatchString(origin) {
return true
Copy link
Member

Choose a reason for hiding this comment

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

is it accurate that matching one origin in a multi-origin request means we should allow the whole chain? I'm not familiar enough with how multi-origin requests work to know if this is right or wrong, but it seems like this would open the door to an allowed origin of https://good.example.com permitting a multi-origin chain of https://good.example.com https://bad.example.com

Copy link
Contributor Author

@tkashem tkashem Jan 20, 2023

Choose a reason for hiding this comment

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

@liggitt After diffing a bit, this is what the RFC says about multiple origins in the Origin header

In some cases, a number of origins contribute to causing the user
agents to issue an HTTP request. In those cases, the user agent MAY
list all the origins in the Origin header field. For example, if the
HTTP request was initially issued by one origin but then later
redirected by another origin, the user agent MAY inform the server
that two origins were involved in causing the user agent to issue the
request.

So theoretically it is possible to have multiple origins in the Origin header, then I looked at the CORS spec here:
https://www.w3.org/TR/2020/SPSD-cors-20200602/#access-control-allow-origin-response-header

Access-Control-Allow-Origin = "Access-Control-Allow-Origin" ":" origin-list-or-null | "*"

In practice the origin-list-or-null production is more constrained. Rather than allowing a space-separated list of origins, it is either a single origin or the string "null".

If the response includes zero or more than one Access-Control-Allow-Origin header values, return fail and terminate this algorithm

I can think of the following options if the Origin header contains multiple origins and CORS is enabled:

  • A: do nothing:
    • if the regexp matches, the CORS filter will include the matched origin in the Access-Control-Allow-Origin response header, not all the origins. (I am going to write a test to observe this behavior)
    • if the regexp does not match - did the user expect a certain origin from the Origin header to match the regular expression (today we use req.Header.Get("Origin") and that returns the first origin.
  • B: if the Origin header has multiple origins we don't send CORS response headers (we can send a Warning to the client)
  • C: if the Origin header has multiple origins, we run the regular expression(s) against each origin and send the first matching origin via Access-Control-Allow-Origin

depending on how we want to approach it, I will open a separate PR, this PR seems orthogonal to this issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added a test in #115242 to understand the current behavior with multiple origins in Origin header(s)

Comment on lines 227 to 242
if len(regexp) == 0 {
continue
}
Copy link
Member

@liggitt liggitt Nov 23, 2022

Choose a reason for hiding this comment

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

is this correct, or is this a bug in their config that produces a regex that matches anything?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, or it could be due to --cors-allowed-origins="", we should return a validation error if that happens

@k8s-ci-robot k8s-ci-robot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Dec 16, 2022
@k8s-ci-robot k8s-ci-robot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jan 20, 2023
@tkashem tkashem force-pushed the cors-cve branch 3 times, most recently from 8d30f80 to d11924d Compare January 20, 2023 19:13
@liggitt
Copy link
Member

liggitt commented Jan 30, 2023

/lgtm
/approve

@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Jan 30, 2023
@k8s-ci-robot
Copy link
Contributor

LGTM label has been added.

Git tree hash: 236e386d33be922f62a0641df636f7855c3f4f00

@liggitt
Copy link
Member

liggitt commented Jan 30, 2023

Thanks, improved validation on the command-line flag lgtm

@k8s-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: liggitt, tkashem

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@k8s-ci-robot k8s-ci-robot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Jan 30, 2023
@k8s-ci-robot k8s-ci-robot merged commit d529048 into kubernetes:master Jan 30, 2023
@k8s-ci-robot k8s-ci-robot added this to the v1.27 milestone Jan 30, 2023
pohly added a commit to pohly/kubernetes that referenced this pull request Jun 26, 2024
kubernetes#112809 tightened what regular
expressions are allowed and now requires that they start matching with a double
dash.
@pohly
Copy link
Contributor

pohly commented Jun 26, 2024

When using hack/local-up-cluster.sh, this parameter is passed:

I0626 18:27:29.515858  689698 flags.go:64] FLAG: --cors-allowed-origins="[/127.0.0.1(:[0-9]+)?$,/localhost(:[0-9]+)?$]"

I think the square brackets can be ignored, the actual parameter value is /127.0.0.1(:[0-9]+)?$,/localhost(:[0-9]+)?$.

kube-apiserver then fails:

E0626 18:27:29.519409  689698 run.go:72] "command failed" err="--cors-allowed-origins has an invalid regular expression: regular expression does not pin to start/end of host in the origin header, help: List of allowed origins for CORS, comma separated. An allowed origin can be a regular expression to support subdomain matching. If this list is empty CORS will not be enabled. Please ensure each expression matches the entire hostname by anchoring to the start with '^' or including the '//' prefix, and by anchoring to the end with '$' or including the ':' port separator suffix. Examples of valid expressions are '//example\\.com(:|$)' and '^https://example\\.com(:|$)'"

Replacing the single slash with double slashes fixes it for me. But I'm wondering whether the check is too strict?

pohly added a commit to pohly/kubernetes that referenced this pull request Jun 26, 2024
kubernetes#112809 tightened what regular
expressions are allowed and now requires that they start matching with a double
dash.
harpreet9ja pushed a commit to harpreet9ja/kubernetes that referenced this pull request Jun 30, 2024
kubernetes#112809 tightened what regular
expressions are allowed and now requires that they start matching with a double
dash.
YamasouA pushed a commit to YamasouA/kubernetes that referenced this pull request Jul 24, 2024
kubernetes#112809 tightened what regular
expressions are allowed and now requires that they start matching with a double
dash.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
approved Indicates a PR has been approved by an approver from all required OWNERS files. area/apiserver cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. kind/bug Categorizes issue or PR as related to a bug. lgtm "Looks good to me", indicates that a PR is ready to be merged. needs-priority Indicates a PR lacks a `priority/foo` label and requires one. release-note Denotes a PR that will be considered when it comes time to generate release notes. sig/api-machinery Categorizes an issue or PR as relevant to SIG API Machinery. size/L Denotes a PR that changes 100-499 lines, ignoring generated files. triage/accepted Indicates an issue or PR is ready to be actively worked on.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants