Skip to content

fix(security): insufficient access controls#6191

Merged
alespour merged 42 commits intomasterfrom
fix/issue-1042
Apr 10, 2026
Merged

fix(security): insufficient access controls#6191
alespour merged 42 commits intomasterfrom
fix/issue-1042

Conversation

@alespour
Copy link
Copy Markdown
Contributor

@alespour alespour commented Mar 25, 2026

  • Reader UI route restriction: restricted Reader frontend navigation to dashboard routes only, matching the issue’s recommendation to block Chronograf URLs except /sources/<datasource>/dashboards. This removes normal in-app access to pages like Data Explorer, Logs, and Manage Sources for Reader users.
  • InfluxQL read-only enforcement: implemented server-side Reader query filtering so only read-safe InfluxQL is allowed, aligning with “allow only SELECT/SHOW for Reader” recommendation. This is parser-based (not string matching), and it explicitly blocks write bypasses like SELECT ... INTO ...
  • Flux write-capability blocking: added Reader-side enforcement on Flux execution paths to deny write-capable query behavior per the issue recommendation to disallow write/admin-capable functions (at least to()). This prevents Reader from using Flux for write operations while preserving read workflows.
  • Reader behavior documentation update: updated docs to reflect the final enforced Reader behavior (dashboard-focused access with read-only query behavior). This keeps product behavior and documentation consistent for operators and reviewers.

Hardening:

  • Server-side SPA route allow list enforcement: added backend enforcement for Reader SPA navigation so direct URL entry cannot bypass frontend-only restrictions. This is an allow list model (/ and /sources/:id/dashboards*), making the frontend restriction robust against manual URL bypass.

Manual tests:

  • OAuth login flow still works (/oauth/.../callback -> /landing no 403).
  • Reader can open dashboards routes normally.
  • Reader is redirected from non-dashboard routes.
  • Direct URL paste to blocked reader routes is still blocked (no frontend bypass). [1]
  • Reader API behavior stays read-only (read endpoints pass, write endpoints denied). [2]
  • InfluxQL guard: reader SELECT ... works, CREATE/DROP/ALTER/GRANT/REVOKE/DELETE/INTO are denied. [3]
  • Mixed/multi-statement InfluxQL with any unsafe statement is denied. [4]
  • Admin/editor can still access/manage non-reader pages normally.


Run in browser DevTools (eg F12 -> Console):

[1]

const api = async (name, method, url, body) => {
    const r = await fetch(url, {
      method,
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json',
        'X-Requested-With': 'XMLHttpRequest',
      },
      body: body ? JSON.stringify(body) : undefined,
    })
    const text = await r.text()
    return {name, status: r.status, body: text.slice(0, 200)}
  }

  const run = async () => {
    const me = await fetch('/chronograf/v1/me', {credentials: 'include'}).then(r => r.json())
    console.log('role =', me.role)

    const cases = [
      // Reader read checks
      ['read source', 'GET', '/chronograf/v1/sources/4'],
      ['read health', 'GET', '/chronograf/v1/sources/4/health'],
      ['read InfluxQL SHOW', 'POST', '/chronograf/v1/sources/4/queries', {
        queries: [{id: '1', query: 'SHOW DATABASES'}],
      }],

      // Reader write-deny checks
      ['deny InfluxQL DROP', 'POST', '/chronograf/v1/sources/4/queries', {
        queries: [{id: '1', query: 'DROP DATABASE smoke'}],
      }],
      ['deny source create', 'POST', '/chronograf/v1/sources', {
        name: 'forbidden',
        type: 'influx',
        url: 'http://localhost:8086',
      }],

      // Flux guard check (Flux-enabled source 2)
      ['deny Flux to()', 'POST', '/chronograf/v1/sources/2/proxy/flux?version=2.7.8&path=%2Fapi%2Fv2%2Fquery%3Forganization%3Dmy-org', {
        query: 'from(bucket:"my-bucket/autogen") |> range(start:-1h) |> v1.to(bucket:"x", org:"my-org")',
        dialect: {annotations: ['group', 'datatype', 'default']},
      }],
    ]

    const out = []
    for (const [name, method, url, body] of cases) {
      out.push(await api(name, method, url, body))
    }
    console.table(out)
    return out
  }

  run()

[2]

const q = async query => {
    const r = await fetch('/chronograf/v1/sources/4/queries', {
      method: 'POST',
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json',
        'X-Requested-With': 'XMLHttpRequest',
      },
      body: JSON.stringify({queries: [{id: '1', query}]}),
    })
    return {query, status: r.status, body: (await r.text()).slice(0, 180)}
  }

  const run = async () => {
    const tests = [
      // allowed
      'SHOW DATABASES',
      'SELECT * FROM "telegraf"."autogen"."cpu" LIMIT 1',

      // denied
      'SELECT * INTO "x"."autogen"."y" FROM "telegraf"."autogen"."cpu" LIMIT 1',
      'CREATE DATABASE testdb',
      'DROP DATABASE testdb',
      'ALTER RETENTION POLICY autogen ON telegraf DURATION 7d REPLICATION 1',
      'GRANT READ ON telegraf TO reader',
      'REVOKE READ ON telegraf FROM reader',
      'DELETE FROM "cpu"',
    ]

    const out = []
    for (const t of tests) out.push(await q(t))

    console.table(out.map(x => ({status: x.status, query: x.query})))
    return out
  }

  run()

[3]

const q = async query => {
    const r = await fetch('/chronograf/v1/sources/4/queries', {
      method: 'POST',
      credentials: 'include',
      headers: {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'},
      body: JSON.stringify({queries: [{id: '1', query}]}),
    })
    return {query, status: r.status, body: (await r.text()).slice(0, 160)}
  }

  const run = async () => {
    const tests = [
      'SHOW DATABASES; SELECT * FROM "telegraf"."autogen"."cpu" LIMIT 1', // should pass
      'SHOW DATABASES; DROP DATABASE testdb',                               // deny
      'SELECT * FROM "telegraf"."autogen"."cpu" LIMIT 1; CREATE DATABASE x', // deny
      'SELECT * FROM "telegraf"."autogen"."cpu" LIMIT 1; SELECT * INTO "x"."autogen"."y" FROM "telegraf"."autogen"."cpu" LIMIT 1', // deny
    ]

    const out = []
    for (const t of tests) out.push(await q(t))
    console.table(out.map(x => ({status: x.status, query: x.query})))
    return out
  }

  run()

[4]

const call = async query => {
    const r = await fetch('/chronograf/v1/sources/4/queries', {
      method: 'POST',
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json',
        'X-Requested-With': 'XMLHttpRequest',
      },
      body: JSON.stringify({queries: [{id: '1', query}]}),
    })
    return {query, status: r.status, body: (await r.text()).slice(0, 180)}
  }

  const tests = [
    ['safe multi', 'SHOW DATABASES; SELECT * FROM "telegraf"."autogen"."cpu" LIMIT 1'],
    ['unsafe DROP', 'SHOW DATABASES; DROP DATABASE testdb'],
    ['unsafe CREATE', 'SELECT * FROM "telegraf"."autogen"."cpu" LIMIT 1; CREATE DATABASE x'],
    ['unsafe INTO', 'SELECT * FROM "telegraf"."autogen"."cpu" LIMIT 1; SELECT * INTO "x"."autogen"."y" FROM "telegraf"."autogen"."cpu" LIMIT 1'],
  ]

  ;(async () => {
    const out = []
    for (const [name, query] of tests) {
      const res = await call(query)
      out.push({name, status: res.status, query})
    }
    console.table(out)
  })()

@alespour alespour changed the title fix(security): enforce Reader least-privilege for routes and query execution fix(security): insufficient access controls Mar 25, 2026
@alespour alespour marked this pull request as ready for review March 25, 2026 16:22
@alespour alespour requested a review from Copilot March 25, 2026 16:22
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens “Reader” access controls across the UI and server to prevent unauthorized SPA navigation and to enforce read-only query execution for both InfluxQL and Flux.

Changes:

  • Added UI route restrictions for Reader users (dashboard-only navigation) and role-gated access to /logs and /sources/new.
  • Implemented server-side enforcement for Reader SPA route allowlisting and read-only query guards for InfluxQL and Flux.
  • Refactored principal-to-user resolution logic to share between auth middleware and the new SPA route guard, and updated docs/tests accordingly.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
ui/test/CheckSource.test.tsx Adds unit tests to validate Reader dashboard-only redirects in CheckSources.
ui/src/index.tsx Adds role-gated route wrappers for /logs and /sources/new.
ui/src/CheckSources.tsx Enforces Reader redirects to dashboards when navigating to non-dashboard source routes.
server/reader_spa_guard.go Introduces server-side Reader SPA allowlist guard for HTML navigations.
server/reader_spa_guard_test.go Adds coverage for allowed/denied SPA navigation and basepath behavior.
server/queries.go Enforces Reader InfluxQL read-only checks on the Queries analysis endpoint.
server/queries_test.go Adds a test ensuring Readers are forbidden from unsafe InfluxQL via /queries.
server/influx.go Enforces Reader InfluxQL read-only checks on the Influx proxy endpoint.
server/influx_test.go Adds a test ensuring Readers are forbidden from unsafe InfluxQL via /proxy.
server/influxql_reader_guard.go Adds parser-based InfluxQL statement allowlisting for Reader role.
server/flux.go Enforces Reader Flux read-only checks on the Flux proxy endpoint.
server/flux_reader_guard.go Adds Flux AST-based detection to block write-capable Flux calls for Readers.
server/flux_reader_guard_test.go Adds tests for Reader Flux restrictions (including to() and parse errors).
server/principal_users.go Adds shared principal→user resolution helper used by multiple middlewares.
server/auth.go Refactors auth middleware to use shared principal→user resolution helper.
server/mux.go Wires the Reader SPA guard into the server middleware chain.
README.md Documents updated Reader behavior (dashboard focus + read-only query enforcement).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@alespour
Copy link
Copy Markdown
Contributor Author

Coding finished. Going through manual tests.

@karel-rehor karel-rehor self-requested a review March 31, 2026 12:45
@karel-rehor
Copy link
Copy Markdown
Contributor

Manual functional testing shows improve enforcement of authorization rules. It also shows no obvious regression issues. All roles seem to work as before while Reader authorization breaches have been fixed.

Copy link
Copy Markdown
Contributor

@karel-rehor karel-rehor left a comment

Choose a reason for hiding this comment

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

Changes all make sense. Looks good to me. 🚴 🏁

@alespour alespour merged commit 0a19008 into master Apr 10, 2026
3 checks passed
@alespour alespour deleted the fix/issue-1042 branch April 10, 2026 14:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants