Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Workspace applications CORS handling prevents external requests to apps #15096

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

Open
Emyrk opened this issue Oct 15, 2024 · 43 comments
Open

Workspace applications CORS handling prevents external requests to apps #15096

Emyrk opened this issue Oct 15, 2024 · 43 comments
Assignees
Labels
customer-requested Features requested by enterprise customers. Only humans may set this. must-do Issues that must be completed by the end of the Sprint. Or else. Only humans may set this. needs decision Needs a higher-level decision to be unblocked. Skydio

Comments

@Emyrk
Copy link
Member

Emyrk commented Oct 15, 2024

Problem

  • External requests to a Coder app will always fail.
  • Workspace apps cannot make requests to each other if they are owned by different users.

Explanation of why

As coder stands today (v2.15.3), we allow cross domain requests between workspace apps of the same user. Meaning you could host a backend webserver on workspace A, and host the frontend on workspace B. As long as the workspaces are owned by the same user, all web requests will function correctly (assuming credentials: "include").

This is all handled magically here

func WorkspaceAppCors(regex *regexp.Regexp, app appurl.ApplicationURL) func(next http.Handler) http.Handler {
return cors.Handler(cors.Options{
AllowOriginFunc: func(r *http.Request, rawOrigin string) bool {
origin, err := url.Parse(rawOrigin)
if rawOrigin == "" || origin.Host == "" || err != nil {
return false
}
subdomain, ok := appurl.ExecuteHostnamePattern(regex, origin.Host)
if !ok {
return false
}
originApp, err := appurl.ParseSubdomainAppURL(subdomain)
if err != nil {
return false
}
return ok && originApp.Username == app.Username
},
AllowedMethods: []string{
http.MethodHead,
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete,
},
AllowedHeaders: []string{"*"},
AllowCredentials: true,
})
}

This allows basic intuitive functionality. By handling this for the user's, we can address some of the nuanced CORS behaviors within Coder. For example, the most common answer on solving CORS is Access-Control-Allow-Origin: *. This will not work for an application in Coder. All Coder apps require Coder authentication via a cookie, and credentials are not supported if CORS is set to *.

From Alice Bob
Workspace 1 Workspace 2 Workspace 3
To App A App B App C App D
Alice Workspace 1 App A * *
App B ✅* *
Workspace 2 App C * *
Bob Workspace 3 App D

🚫 What is the status quo?

The status quo is that Coder manages CORS requests between apps. This approach has generally lead to less customer support tickets on how to handle CORS.

We currently do not allow requests between different users. This does not appear to be because of huge security risk. Subdomain apps are given very limited session tokens that only allow workspace app connections. There is a risk that someone could steal this token and go to other apps by the user, but they cannot interact with other Coder resources.

Solution #1 Solve it in Coder (INCOMPLETE SOLUTION)

We can adjust the table above to include cross user requests. This would expand the security surface by default, which although attacks are limited in escalation, providing this by default seems insecure.

It also does not handle requests external to Coder. External requests must be supported by public shared apps to handle the use cases requested.

🚫 Solution #2 Suggest workarounds (Not always possible)

Instead of allowing cross site requests, the user could deploy a local webserver to proxy requests to the second user's app.

Many frontend servers have this as a feature:

If the web app in question has a backend server, then the backend server can proxy traffic to avoid CORS issues.

❓ Solution #3 Disable CORS handling by Coder

The best option is to disable our custom CORS handling and defer to the workspace app. There is a few ways we could maybe do this:

Disable at the deployment level.

  • Adds another server flag 👎
  • Breaks the QOL behavior for likely 1 use case 👎
  • Easy to switch behavior without any state (db) lookups 👍

Disable at the app level

  • Could configure in terraform per app 👍
  • Have to do a db lookup on requests or maintain a cache 👎

Disable based on response headers.
Given CORS requests are preflight requests, can we do this? Can we pass the preflight to the workspace app, get the headers. If CORS headers are present, return as is. If they are not, add them to the response? Then we just defer to the app if they configure it themselves. No Coder config necessary.

Final thoughts

When workspace sharing becomes a feature, this use case might show up more.

Implementation notes

The implementation cannot be configured at the workspace, template, or user level. To be configured there, an in memory cache would be required.

The requirements to a solution cannot require a database lookup on each request. That would cause too many database calls for web apps with hundreds of static resources.

The only input information on each request you get from making a request from app A to app B.

  • The origin, https://8002--<agent>--<workspace>--<user_A>--apps.coder.com/
  • The request to B, https://8002--<agent>--<workspace>--<user_B>--apps.coder.com/ with cookies for the app B

Related Links:

@coder-labeler coder-labeler bot added the needs decision Needs a higher-level decision to be unblocked. label Oct 15, 2024
@dannykopping dannykopping changed the title Cross workspace application requests blocked to CORs across user accounts Cross workspace application requests blocked to CORS across user accounts Oct 15, 2024
@MrPeacockNLB
Copy link
Contributor

Nice to see that this is now further developed. I ask a long time ago Coder support for this!

In the mean time we are using a simple ingress to bypass coder completely as it was not flexible enough.

@MrPeacockNLB
Copy link
Contributor

Our UseCase was to Develop WebComponents in a Coder Workspace which then gets included into a deployed web portal. This was not possible due CORS issues.

@Emyrk
Copy link
Member Author

Emyrk commented Oct 18, 2024

Our UseCase was to Develop WebComponents in a Coder Workspace which then gets included into a deployed web portal. This was not possible due CORS issues.

That use case would still not work even under the new proposal. The CORs handling is only between workspace apps. It still would block requests that are exiting Coder.

@spikecurtis
Copy link
Contributor

CoderVPN can be helpful here, in that you can directly interact with a web app on your workspace and set whatever CORS policy you'd like on it.

@spikecurtis
Copy link
Contributor

The implementation cannot be configured at the workspace, template, or user level. To be configured there, an in memory cache would be required.

The requirements to a solution cannot require a database lookup on each request. That would cause too many database calls for web apps with hundreds of static resources.

I agree we can't do a DB lookup on each request, but I think a per-workspace policy cache would give acceptable performance. It adds complexity, which needs to be balanced against the utility of configuration at workspace, template, or user scope.

@Emyrk
Copy link
Member Author

Emyrk commented Oct 21, 2024

@spikecurtis yes. I was thinking either per workspace or per template.

I wonder too if request cookie headers come across in CORS pre-flight requests. If so, we have a cookie on all subdomain apps (that are not public), that define the app. It looks like this inside a jwt. I just realized you need to auth to the subdomain first to have the cookie set. So this only works if the user visits the subdomain first. And this breaks even more if an external app from outside a browser context hits the app (assuming it is public)

  "request": {
    "access_method": "subdomain",
    "base_path": "/",
    "app_prefix": "",
    "username_or_id": "emyrk",
    "workspace_name_or_id": "april",
    "agent_name_or_id": "dev",
    "app_slug_or_port": "filebrowser"
    # CORS level here?
  },

We could maybe define it on the app level, if the app is not public (or we set this cookie regardless of auth?). We sign it with a local key, so forging a cookie is not possible.

@Emyrk
Copy link
Member Author

Emyrk commented Oct 21, 2024

@MrPeacockNLB to confirm, is that web portal external to Coder? And is that web portal static?

What sort of CORS policy is required for your functionality? Access-Control-Allow-Origin: *? Or is it specific domain(s) known in advance? Access-Control-Allow-Origin: https://some.domain.com?

@Emyrk
Copy link
Member Author

Emyrk commented Oct 21, 2024

Including some more information from another user. They have a use case to use a Coder app as the API service for some external service.

When dealing with external services, we can either support some static list, or we will have to defer to the application. I do not see a way to implement a dynamic filter in any sensible way.

Maybe if we configure this specific to an app, a workspace, or a template, we have an option to not strip the original CORS headers. Just pass through whatever the app does by default.

@Emyrk Emyrk self-assigned this Oct 21, 2024
@Emyrk
Copy link
Member Author

Emyrk commented Oct 21, 2024

CoderVPN can be helpful here, in that you can directly interact with a web app on your workspace and set whatever CORS policy you'd like on it.

This might not work for all use cases. One reported use case is to make requests to the Coder app from some external site/server. So the user is not in the equation at all. A VPN would only expose apps to other localhost servers. This is only made possible because share level public is a thing. So no auth is required.

@Emyrk Emyrk changed the title Cross workspace application requests blocked to CORS across user accounts Cross workspace application requests blocked to CORS across user accounts and external requests Oct 21, 2024
@Emyrk Emyrk added the customer-requested Features requested by enterprise customers. Only humans may set this. label Oct 21, 2024
@spikecurtis
Copy link
Contributor

One reported use case is to make requests to the Coder app from some external site/server. So the user is not in the equation at all. A VPN would only expose apps to other localhost servers. This is only made possible because share level public is a thing. So no auth is required.

CoderVPN allows access to the Coder workspace app at the DNS name <workspace>.coder, so it would be accessible from an external site/server if the right CORS policy is configured on the app. Coder is not involved at the HTTP layer at all in this case, so the developer who is working on the app can set their own CORS policy. It's only accessible to the user that owns the workspace tho.

If the goal is to make a public application via Coder, then you'd need to use the Coder proxy or set up some other way to do ingress into the workspace.

@Emyrk Emyrk changed the title Cross workspace application requests blocked to CORS across user accounts and external requests Workspace applications CORS handling prevents external requests to apps Oct 22, 2024
@Emyrk
Copy link
Member Author

Emyrk commented Oct 22, 2024

If the goal is to make a public application via Coder, then you'd need to use the Coder proxy or set up some other way to do ingress into the workspace.

Yup this.

I am wondering if we can just detect an existing CORS header, and if present, do not strip or append our own. This would require us to pass the preflight request to the app, then modify the response if it omits the headers.

@Emyrk Emyrk removed their assignment Oct 22, 2024
@bpmct
Copy link
Member

bpmct commented Oct 31, 2024

I'm in favor of solution 3 if we can do it at the template level. One disadvantage of the app level is it does not allow shared ports or any ports to be used.

If we did it deployment-wide, it would probably break many use cases.

@bpmct
Copy link
Member

bpmct commented Oct 31, 2024

@mtojek @johnstcn Is this also something 🥥 can take?

@Emyrk
Copy link
Member Author

Emyrk commented Oct 31, 2024

@bpmct I wonder if we can Solution 3 Disable based on response headers. Just disable if the headers are already set.

@johnstcn
Copy link
Member

@mtojek @johnstcn Is this also something 🥥 can take?

🥥 is on it!

@spikecurtis
Copy link
Contributor

spikecurtis commented Nov 1, 2024

I wonder if we can Solution 3 Disable based on response headers. Just disable if the headers are already set.

We would still need to rewrite the header if they set Access-Control-Allow-Origin: * on a non-public port, right?

@Emyrk
Copy link
Member Author

Emyrk commented Nov 1, 2024

I wonder if we can Solution 3 Disable based on response headers. Just disable if the headers are already set.

We would still need to rewrite the header if they set Access-Control-Allow-Origin: * on a non-public port, right?

If we decide not to, then the app can never have a cross origin request because credentials will never be included. But we could let it be their mistake, although the solution is a bit nuanced because it requires knowing you need the Coder cookie for access. Would be amazing if we could return some error message in the preflight response. But that requires us to basically anticipate a CORS error based on the user_agent and other headers 😢.

Solution 3 is the most basic, but it is the most transparent and hard to debug. I don't think we can even add any headers to attach meta data on the behavior, because CORS has to whitelist headers (if not using Access-Control-Allow-Headers: *)

@dannykopping dannykopping self-assigned this Nov 18, 2024
@Emyrk
Copy link
Member Author

Emyrk commented Nov 19, 2024

Chat with @dannykopping @johnstcn summary.

Add the CORS configurable behavior on this cookie jwt:

type Request struct {
AccessMethod AccessMethod `json:"access_method"`
// BasePath of the app. For path apps, this is the path prefix in the router
// for this particular app. For subdomain apps, this should be "/". This is
// used for setting the cookie path.
BasePath string `json:"base_path"`
// Prefix is the prefix of the subdomain app URL. Prefix should have a
// trailing "---" if set.
Prefix string `json:"app_prefix"`
// For the following fields, if the AccessMethod is AccessMethodTerminal,
// then only AgentNameOrID may be set and it must be a UUID. The other
// fields must be left blank.
UsernameOrID string `json:"username_or_id"`
// WorkspaceAndAgent xor WorkspaceNameOrID are required.
WorkspaceAndAgent string `json:"-"` // "workspace" or "workspace.agent"
WorkspaceNameOrID string `json:"workspace_name_or_id"`
// AgentNameOrID is not required if the workspace has only one agent.
AgentNameOrID string `json:"agent_name_or_id"`
AppSlugOrPort string `json:"app_slug_or_port"`
}
This is the information cached for all requests, so we won't need to manage a separate cache as previously thought.

CORS behavior should be configurable on the app section in terraform.


An idea: the CORS behavior could be configurable by some enum in terraform on the app:

CORS Enum:

  • cors_behavior: 'user/deployment/global/passthrough' (TBD all naming)

@dannykopping
Copy link
Contributor

To be clear, we're opting for Solution 3 :: Disable at the app level.

We're going to allow template authors to optionally enable passthru of all CORS requests to the "upstream" app, which will be solely responsible for responding to those requests; coderd will only be responsible for authentication based on the app token provided in the cookie and proxying the requests/responses on.

@bpmct
Copy link
Member

bpmct commented Nov 20, 2024

Will this also work with shared (or non-shared) ports that are NOT coder_apps? The reason I was in favor of template-level is that many template authors will not be aware of which ports developers are using.

@Emyrk
Copy link
Member Author

Emyrk commented Nov 20, 2024

@bpmct I do not think the current solution does. We'd have to also add it as a column on the shared ports table, and make some ui element for the toggle?

@dannykopping
Copy link
Contributor

@bpmct how do you see this being defined at a template level? As a toggle in the UI only, or via some terraform as well?
I'm not sure which resource this would fit into, unless I'm missing something obvious?

Or do we instead allow the user to configure CORS handling at the last moment of responsibility when they actually share a port?

--

As an aside, and please forgive my ignorance, did we ever consider adding shared ports as a top-level terraform resource, or is it expected that folks created a coder_app for those? i.e. if a webserver is installed on the workspace and it needs to be accessible, should the workspace owner manually make that port shared OR create a coder_app? Are those the only two options?

@dannykopping
Copy link
Contributor

@bpmct that's the workspace view; did you perhaps share the wrong image? I assume you want this set as a policy for all workspaces using a template.

@dannykopping
Copy link
Contributor

@bpmct @Emyrk something like this perhaps?

Image

@bpmct
Copy link
Member

bpmct commented Nov 21, 2024

I like that! Legit perfect.

@bpmct that's the workspace view; did you perhaps share the wrong image? I assume you want this set as a policy for all workspaces using a template.

Ah my image was just showing the ports dropdown and how it is different from coder_app and important to support. I agree the config should be template-level like your screenshot

@Emyrk
Copy link
Member Author

Emyrk commented Nov 21, 2024

@dannykopping that is on the template level, however I thought it was discussed to implement it per-app. Meaning it would have to be on the port sharing drop down somehow.

If we make this template level, then all apps share the same CORs behavior. In the use cases described, I think it's preferred to have a singular app be exempt, but other apps keep the nice default behavior.

@dannykopping
Copy link
Contributor

@Emyrk I think it's a case of AND rather than OR, based on what @bpmct said here - no?

@Emyrk
Copy link
Member Author

Emyrk commented Nov 21, 2024

@Emyrk I think it's a case of AND rather than OR, based on what @bpmct said here - no?

Ah. It just feels a bit strange to assert at the template level for all ports imo. Per app makes sense, but to apply at the template level means users are going to have to send us their template CORS setting when submitting issues. It just feels disconnected from what it is actually affecting.

@bpmct
Copy link
Member

bpmct commented Nov 21, 2024

The only requirement from the product side is that we also support this for both coder_apps AND the ports dropdown since I imagine 80% of users will use the URLs from the ports dropdown (non-named coder_apps) to do requests.

I don't have opinions on whether we need to support both template level AND per-coder_app. If there are use cases that you think are important (e.g. securing code-server app), then we'll need to do both... Maybe the template setting only applies to the ports dropdown and then each coder_app gets its unique config? Or we modify the ports dropdown to also have configurable CORS policies. That feels overkill which is why my first instinct was just one template-level setting

@dannykopping
Copy link
Contributor

Thanks for the additional context, that helps.

@Emyrk @bpmct could we side-step this whole problem and insist that customers create a coder_app if they need to control CORS?

If not, I will add this add the port-share level. My only hesitation here is the UI will need to be updated, and honestly the UX of this component feels a bit awkward right now.

Users just have to know to click the lock icon to access the port-share, even though there's no UI indication that it's a button.

Image

We'll have to make the whole panel a bit wider to accommodate the CORS behavior, and/or use some icons to indicate the behavior.
@chrifro your opinion here would be much appreciated.

@chrifro
Copy link

chrifro commented Nov 26, 2024

Users just have to know to click the lock icon to access the port-share, even though there's no UI indication that it's a button.

Uh yes, that's not intuitive at all

We can definitely give the whole port menu a UI/UX polish when looking into this. Let me know once you have agreed on where the CORS settings should live and I can work on some designs.

@bpmct
Copy link
Member

bpmct commented Nov 26, 2024

@Emyrk @bpmct could we side-step this whole problem and insist that customers create a coder_app if they need to control CORS?

Nope we can't. Template admins and developers are totally different personas. Template admins in most cases will not understand what ports developers will be using since thousands of developers across different teams will be writing different apps.

@Emyrk
Copy link
Member Author

Emyrk commented Nov 26, 2024

@bpmct an argument for just the templates (to play devils advocate) is CORS is a security measure. And template admins should be able to control the security surface for the given template. 🤷‍♂

Saying that, I think the behavior should live in both. Unfortunate the UX will have to fit even more stuff.

@bpmct
Copy link
Member

bpmct commented Nov 26, 2024

A template-level setting is my preference. That way, it is up to the template admin:

Image

@dannykopping
Copy link
Contributor

@Emyrk @bpmct could we side-step this whole problem and insist that customers create a coder_app if they need to control CORS?

Nope we can't. Template admins and developers are totally different personas. Template admins in most cases will not understand what ports developers will be using since thousands of developers across different teams will be writing different apps.

Got it; I thought that CORS behavior changes would only be for well-known apps but I guess it could be for anything.

@bpmct an argument for just the templates (to play devils advocate) is CORS is a security measure. And template admins should be able to control the security surface for the given template. 🤷‍♂

Saying that, I think the behavior should live in both. Unfortunate the UX will have to fit even more stuff.

Can we start in the template (which is the simpler option), and expand to port-shares later if we get enough interest?

@bpmct
Copy link
Member

bpmct commented Nov 26, 2024

I suppose, I just can't imagine a scenario where a coder_app from one workspace needs to CORS to a coder_app on another users workspace.

I figured this was for user<>user collaboration on their projects. Such as a frontend developer using a backend's developer's port. I doubt the template admin will know/hardcode such ports as coder_apps. Heck, we don't and our template is for a single repo.

@bpmct
Copy link
Member

bpmct commented Nov 26, 2024

Just reviewed the 2 open tickets we have for this and they both refer to shared ports.

Edit: One does, not sure if the other does.

@bpmct
Copy link
Member

bpmct commented Nov 26, 2024

Ok from the chat:

  • Doing this via shared ports is important as that is where the vast majority of applications will be running/shared. 80%+. coder_app is a partial solution
  • We learned that other CDE products "pass through" CORS across the board and leave it up to the application
  • Because of that, we are going to introduce a template-level setting to allow template admins to "remove the magic"
    Image.
  • At the same time, we'll likely introduce app: add cors_behavior attribute terraform-provider-coder#309 so that if "passthrough" is set, somebody could still enable the magic for a specific application or two.

@bpmct
Copy link
Member

bpmct commented Nov 26, 2024

Moving this into the next sprint instead of this one.

@dannykopping
Copy link
Contributor

I've pushed up my changes to change coder_app behavior: #15669
As discussed, we'll block the merging of this PR until we have the functionality in place for port shares.

@bpmct
Copy link
Member

bpmct commented Dec 12, 2024

Removing this from the sprint, we'll be able to pick it up in the new year at some point.

@mtojek
Copy link
Member

mtojek commented Jan 20, 2025

Closing this issue.

This issue has remained open and assigned for an extended period, but higher-priority tasks have taken precedence. As discussed in our recent Weekly call, we lack the bandwidth to conduct the rigorous testing required for the changes proposed in here.

Looking ahead, if priorities shift or the need arises to revisit this logic, it would be ideal to address it comprehensively -implementing E2E tests and releasing the improvements. For now, I’m closing this issue as unresolved.

@mtojek mtojek closed this as not planned Won't fix, can't repro, duplicate, stale Jan 20, 2025
@bpmct bpmct reopened this Apr 8, 2025
@bpmct bpmct added the must-do Issues that must be completed by the end of the Sprint. Or else. Only humans may set this. label Apr 8, 2025
@Jphalan Jphalan added the Skydio label May 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
customer-requested Features requested by enterprise customers. Only humans may set this. must-do Issues that must be completed by the end of the Sprint. Or else. Only humans may set this. needs decision Needs a higher-level decision to be unblocked. Skydio
Projects
None yet
Development

No branches or pull requests

10 participants