CVE-2022-24829 (TypeScript, published 2022-04-11T20:15Z)
Description: value: Garden is an automation platform for Kubernetes development and testing. In versions prior to 0.12.39 multiple endpoints did not require authentication. In some operating modes this allows for an attacker to gain access to the application erroneously. The configuration is leaked through the /api endpoint on the local server that is responsible for serving the Garden dashboard. At the moment, this server is accessible to 0.0.0.0 which makes it accessible to anyone on the same network (or anyone on the internet if they are on a public, static IP). This may lead to the ability to compromise credentials, secrets or environment variables. Users are advised to upgrade to version 0.12.39 as soon as possible. Users unable to upgrade should use a firewall blocking access to port 9777 from all untrusted network machines.
Severity: MEDIUM (CVSS v3 base score: 9.8)
Weakness: CWE-306 \u2014 Missing Authentication for Critical Function
The product does not perform any authentication for functionality that requires a provable user identity or consumes a significant amount of resources.
Repository: https://github.com/garden-io/garden
Files changed:
core/src/cli/cli.ts
core/src/constants.ts
core/src/events.ts
core/src/server/dashboard-event-stream.ts
core/src/server/server.ts
core/test/unit/src/cli/cli.ts
core/test/unit/src/server/dashboard-event-stream.ts
core/test/unit/src/server/server.ts
dashboard/src/api/actions.ts
dashboard/src/api/api.ts
dashboard/src/components/menu.tsx
dashboard/src/hooks.ts
dashboard/src/util/helpers.ts
Commit message: fix(core): require auth key for server endpoints


Vulnerable code:
            command.server.showUrl(dashboardProcess?.serverHost || undefined)
    servers: { host: string; command: string }[]
        servers: servers.map((p) => ({ command: p.command!, host: p.serverHost! })),
    this.authKey = randomString(64)
      for (const { host, command } of servers) {
          this.showUrl(host)
      this.server = this.app.listen(this.port)
          this.server = this.app.listen(this.port)
  getUrl() {
      // TODO: require auth key here from 0.13.0 onwards
      const authHeader = ctx.header[authTokenHeader]

      if (authHeader !== this.authKey) {
        ctx.status = 401
        return
      }


  private notReady(ctx: Router.IRouterContext) {
      // TODO: require auth key on connections here, from 0.13.0 onwards

        serverHost: server.getUrl(),
        serverHost: server.getUrl(),
        { host: serverA.getUrl(), clientAuthToken: serverA.authKey, enterprise: false },
        { host: serverB.getUrl(), clientAuthToken: serverB.authKey, enterprise: false },
      garden.events.expectEvent("serversUpdated", { servers: [{ host: values.serverHost, command: "dashboard" }] })
      servers: [{ host: "http://localhost:9800", command: "dashboard" }],
    expect(status).to.equal(`Garden dashboard running at http://localhost:9800`)
      await request(server).get("/").expect(200)
      await request(server).post("/api").send("foo").expect(400)
      await request(server).post("/api").send({ foo: "bar" }).expect(400)
      await request(server).post("/api").send({ command: "foo" }).expect(404)
      await request(server).post("/api").send({ command: "get.config" }).expect(503)
      const res = await request(server).post("/api").send({ command: "get.config" }).expect(200)
      const res = await request(server).get("/dashboardPages/test-plugin/test").expect(302)
      ws = new WebSocket(`ws://localhost:${port}/ws`)
import { getTestKey } from "../util/helpers"
        url: page.url || `/dashboardPages/${provider.name}/${page.name}`,
  const headers = { "Content-Type": "application/json" }
import { NavLink as ReactRouterNavLink } from "react-router-dom"
const NavLink = (props: any) => <ReactRouterNavLink {...props} activeStyle={{ color: colors.gardenGreenDark }} />
          <NavLink to="/">
    const ws = new WebSocket(`ws://${url}/ws`)

Fixed code:
            let url: string | undefined
            if (dashboardProcess) {
              url = `${dashboardProcess.serverHost}?key=${dashboardProcess.serverAuthKey}`
            }
            command.server.showUrl(url)
  GARDEN_SERVER_HOSTNAME: env.get("GARDEN_SERVER_HOSTNAME").required(false).asUrlString(),
    servers: { host: string; command: string; serverAuthKey: string }[]
        servers: servers.map((p) => ({
          command: p.command!,
          host: p.serverHost!,
          serverAuthKey: p.serverAuthKey || "",
        })),
    this.authKey = randomString(24)
      for (const { host, command, serverAuthKey } of servers) {
          this.showUrl(`${host}?key=${serverAuthKey}`)
    const hostname = gardenEnv.GARDEN_SERVER_HOSTNAME || "localhost"

      this.server = this.app.listen(this.port, hostname)
          this.server = this.app.listen(this.port, hostname)
  getBaseUrl() {
  getUrl() {
    return `${this.getBaseUrl()}?key=${this.authKey}`
  }

    http.use((ctx, next) => {
      const authToken = ctx.header[authTokenHeader] || ctx.query.key

      if (authToken !== this.authKey) {
        ctx.throw(401, `Unauthorized request`)
        return
      }
      return next()
    })


  private notReady(ctx: Router.IRouterContext | Koa.ParameterizedContext) {
      // TODO: Only allow auth key authentication
      if (ctx.query.sessionId !== `${this.garden.sessionId}` && ctx.query.key !== `${this.authKey}`) {
        error(`401 Unauthorized`)
        websocket.terminate()
        return
      }

        serverHost: server.getBaseUrl(),
        serverHost: server.getBaseUrl(),
        { host: serverA.getBaseUrl(), clientAuthToken: serverA.authKey, enterprise: false },
        { host: serverB.getBaseUrl(), clientAuthToken: serverB.authKey, enterprise: false },
      garden.events.expectEvent("serversUpdated", {
        servers: [{ host: values.serverHost, command: "dashboard", serverAuthKey: "foo" }],
      })
      servers: [{ host: "http://localhost:9800", command: "dashboard", serverAuthKey: "foo" }],
    expect(status).to.equal(`Garden dashboard running at http://localhost:9800?key=foo`)
      await request(server)
        .get("/")
        .set({ [authTokenHeader]: gardenServer.authKey })
        .expect(200)
    it("returns 401 if missing auth header", async () => {
      await request(server).post("/api").send({}).expect(401)
    })

    it("returns 401 if auth header doesn't match auth key", async () => {
      await request(server)
        .post("/api")
        .set({ [authTokenHeader]: "foo" })
        .send({})
        .expect(401)
    })

      await request(server)
        .post("/api")
        .set({ [authTokenHeader]: gardenServer.authKey })
        .send("foo")
        .expect(400)
      await request(server)
        .post("/api")
        .set({ [authTokenHeader]: gardenServer.authKey })
        .send({ foo: "bar" })
        .expect(400)
      await request(server)
        .post("/api")
        .set({ [authTokenHeader]: gardenServer.authKey })
        .send({ command: "foo" })
        .expect(404)
      await request(server)
        .post("/api")
        .set({ [authTokenHeader]: gardenServer.authKey })
        .send({ command: "get.config" })
        .expect(503)
      const res = await request(server)
        .post("/api")
        .set({ [authTokenHeader]: gardenServer.authKey })
        .send({ command: "get.config" })
        .expect(200)
        .set({ [authTokenHeader]: gardenServer.authKey })
    it("returns 401 if missing auth header", async () => {
      await request(server).get("/dashboardPages/test-plugin/test").expect(401)
    })

    it("returns 401 if auth header doesn't match auth key", async () => {
      await request(server)
        .get("/dashboardPages/test-plugin/test")
        .set({ [authTokenHeader]: "foo" })
        .send({})
        .expect(401)
    })

      const res = await request(server)
        .get("/dashboardPages/test-plugin/test")
        .set({ [authTokenHeader]: gardenServer.authKey })
        .expect(302)
      ws = new WebSocket(`ws://localhost:${port}/ws?sessionId=${garden.sessionId}`)
    it("terminates the connection if auth query params are missing", (done) => {
      const badWs = new WebSocket(`ws://localhost:${port}/ws`)
      badWs.on("close", () => {
        done()
      })
    })

    it("terminates the connection if key doesn't match and sessionId is missing", (done) => {
      const badWs = new WebSocket(`ws://localhost:${port}/ws?key=foo`)
      badWs.on("close", () => {
        done()
      })
    })

    it("terminates the connection if sessionId doesn't match and key is missing", (done) => {
      const badWs = new WebSocket(`ws://localhost:${port}/ws?sessionId=foo`)
      badWs.on("close", () => {
        done()
      })
    })

    it("terminates the connection if both sessionId and key are bad", (done) => {
      const badWs = new WebSocket(`ws://localhost:${port}/ws?sessionId=foo&key=bar`)
      badWs.on("close", () => {
        done()
      })
    })

import { getAuthKey, getTestKey } from "../util/helpers"
        url: page.url || `/dashboardPages/${provider.name}/${page.name}?key=${getAuthKey()}`,
import { getAuthKey } from "../util/helpers"
  const headers = { "Content-Type": "application/json", "x-access-auth-token": getAuthKey() }

import { NavLink as ReactRouterNavLink, NavLinkProps as ReactRouterNavLinkProps } from "react-router-dom"
import { getAuthKey } from "../util/helpers"
type NavLinkProps = Omit<ReactRouterNavLinkProps, "to"> & {
  to: {
    pathname: string
    search?: string
    hash?: string
    state?: any
  }
}

/**
 * Helper class for ensuring that the auth key param is set on routes.
 */
const NavLink = (props: NavLinkProps) => {
  const urlParams = new URLSearchParams(props.to.search)
  urlParams.set("key", getAuthKey() || "")
  const to = {
    ...props.to,
    search: urlParams.toString(),
  }

  return <ReactRouterNavLink {...props} to={to} activeStyle={{ color: colors.gardenGreenDark }} />
}
          <NavLink to={{ pathname: "/" }}>
import { getAuthKey } from "./util/helpers"
    const ws = new WebSocket(`ws://${url}/ws?key=${getAuthKey()}`)

/**
 * Returns the auth key from the current page's URL params.
 */
export function getAuthKey() {
  const urlParams = new URLSearchParams(window.location.search)
  return urlParams.get("key")
}