Skip to content

security: strip ANSI/OSC escapes from API-controlled output#479

Merged
jeremy merged 6 commits into
mainfrom
security/02-output-ansi-sanitize
Jul 14, 2026
Merged

security: strip ANSI/OSC escapes from API-controlled output#479
jeremy merged 6 commits into
mainfrom
security/02-output-ansi-sanitize

Conversation

@jeremy

@jeremy jeremy commented May 30, 2026

Copy link
Copy Markdown
Member

Strip ANSI/OSC escapes from API-controlled output

(Threat: T4 terminal injection via API-controlled strings carrying ANSI/OSC sequences.)

API-controlled strings (project/person/entity names, summaries, notices) reached lipgloss.Render/stdout and the --watch TUI without ansi.Strip, in render paths the generic table/cell path already protected.

Fix

  • Sink-only: sanitize at every terminal sink, not at the source. Summary/Notice/Diagnostic are stored verbatim by WithSummary/WithNotice/WithDiagnostic (so machine/JSON output preserves byte fidelity, consistent with error/hint/data) and stripped only where they're emitted to a terminal — the styled and markdown renderers and the quiet-mode stderr diagnostic (render.go, envelope.go).
  • --watch TUI: strip creator/action/title/excerpt + the person/project description (timeline.go); rune-truncation otherwise preserves escape bytes and the alt-screen executes OSC sequences.
  • Presenter: strip in formatText/formatPerson/formatPeople and the rendered affordance commands (presenter/format.go, presenter/render.go).

Tests

Tests assert JSON summary/notice are verbatim and that every terminal sink (styled/markdown/plain RenderError, summary/notice, breadcrumbs, attachment metadata, and the quiet-mode stderr notice:) neutralizes C1/CSI/OSC escapes.


Part 2/6 of the stacked security series. Base: security/01-api-token-leak.

📚 Stack (merge bottom-up)

  1. security: reject foreign-host URLs in api to prevent token leak #478 — reject foreign-host URLs in api (base main)
  2. security: strip ANSI/OSC escapes from API-controlled output #479 — strip ANSI/OSC escapes from output
  3. security: harden OAuth discovery and token endpoints #480 — harden OAuth discovery / token endpoints
  4. security: close config trust-boundary gaps and gate completion loader #481 — close config trust-boundary gaps + completion gate
  5. security: tighten config-dir perms and validate plugin scope argv #482 — tighten config-dir perms + plugin scope argv
  6. security: pin the gitleaks download with a SHA-256 checksum #483 — bump toolchain/x-net + CI gates

Each is independent except #482 depends on #481 (shared root.go). #478 can land first/alone.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No issues found across 6 files

Re-trigger cubic

Copilot AI left a comment

Copy link
Copy Markdown

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 mitigates terminal-injection risk by stripping ANSI/OSC escape sequences from API-controlled output before it reaches styled, markdown, presenter, and watch-mode render paths.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Changes:

  • Adds source-level sanitization for response summaries, notices, and diagnostics.
  • Adds sink-level stripping in styled/markdown renderers and presenter affordance formatting.
  • Sanitizes timeline watch TUI fields before rendering/truncation and adds coverage for response option sanitization.

Reviewed changes

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

Show a summary per file
File Description
internal/output/envelope.go Strips ANSI/OSC escapes in response options and entity presenter summary/notice output.
internal/output/render.go Applies defense-in-depth stripping in styled and markdown response renderers.
internal/output/output_test.go Adds tests for summary/notice/diagnostic source-level sanitization.
internal/presenter/format.go Sanitizes formatted API-controlled text and person names.
internal/presenter/render.go Sanitizes rendered affordance commands after template interpolation.
internal/commands/timeline.go Sanitizes watch TUI event fields and project/person descriptions.

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

Comment thread internal/commands/timeline.go Outdated
@jeremy jeremy force-pushed the security/01-api-token-leak branch from b6d427d to 2eacce7 Compare May 30, 2026 06:05
@jeremy jeremy force-pushed the security/02-output-ansi-sanitize branch from 7b1d3ea to 61aea03 Compare May 30, 2026 06:05

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 61aea0335f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/presenter/render.go Outdated
Copilot AI review requested due to automatic review settings May 30, 2026 06:13
@jeremy jeremy force-pushed the security/02-output-ansi-sanitize branch from 61aea03 to e06c5c2 Compare May 30, 2026 06:13
@github-actions github-actions Bot added the bug Something isn't working label May 30, 2026

Copilot AI left a comment

Copy link
Copy Markdown

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 6 out of 6 changed files in this pull request and generated 2 comments.

Comment thread internal/presenter/format.go Outdated
Comment thread internal/presenter/format.go Outdated
@jeremy jeremy force-pushed the security/01-api-token-leak branch from 2eacce7 to 447cbf3 Compare May 30, 2026 06:23
@jeremy jeremy force-pushed the security/02-output-ansi-sanitize branch from e06c5c2 to 9103d13 Compare May 30, 2026 06:23

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9103d138ee

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/presenter/format.go Outdated
@jeremy jeremy force-pushed the security/01-api-token-leak branch from 447cbf3 to 2734359 Compare May 30, 2026 07:51
@jeremy jeremy force-pushed the security/02-output-ansi-sanitize branch from 9103d13 to dd63a13 Compare May 30, 2026 07:51

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dd63a1322a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/presenter/format.go Outdated
Copilot AI review requested due to automatic review settings May 30, 2026 08:27
@jeremy jeremy force-pushed the security/02-output-ansi-sanitize branch from dd63a13 to 4566262 Compare May 30, 2026 08:27

Copilot AI left a comment

Copy link
Copy Markdown

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 9 out of 9 changed files in this pull request and generated 1 comment.

Comment thread internal/presenter/render.go Outdated
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Copilot AI left a comment

Copy link
Copy Markdown

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 11 out of 11 changed files in this pull request and generated 1 comment.

Comment thread internal/output/render.go
Copilot AI review requested due to automatic review settings July 14, 2026 02:51
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Copilot AI left a comment

Copy link
Copy Markdown

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 11 out of 11 changed files in this pull request and generated no new comments.

@jeremy jeremy force-pushed the security/01-api-token-leak branch from fc7bafa to e56fa0c Compare July 14, 2026 05:19
Copilot AI review requested due to automatic review settings July 14, 2026 05:19

Copilot AI left a comment

Copy link
Copy Markdown

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 11 out of 11 changed files in this pull request and generated 8 comments.

Comment thread internal/output/render.go Outdated
Comment thread internal/output/render.go
Comment thread internal/output/render.go Outdated
Comment thread internal/output/render.go
Comment thread internal/output/envelope.go Outdated
Comment thread internal/output/envelope.go Outdated
Comment thread internal/output/envelope.go Outdated
Comment thread internal/output/envelope.go Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d0451d413b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/output/render.go Outdated
Comment thread internal/output/render.go

Copilot AI left a comment

Copy link
Copy Markdown

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 11 out of 11 changed files in this pull request and generated 3 comments.

Comment thread internal/commands/timeline.go
Comment thread internal/commands/timeline.go Outdated
Comment thread internal/commands/timeline.go Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 95074c40ae

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/output/render.go
Comment thread internal/output/envelope.go Outdated
jeremy added 5 commits July 13, 2026 23:22
parsePath now validates absolute URLs against the configured base URL host:
a path is accepted only if it's relative or an absolute Basecamp URL on the
same host (path extracted, account segment dropped — the SDK re-prefixes the
configured account). Absolute URLs on any other host are rejected before any
network call, so the SDK never attaches the Authorization: Bearer header to a
foreign host. A stray leading slash ('/https://evil/…') and mixed-case schemes
('HTTPS://…') are normalized first so neither can smuggle an absolute URL past
the host check. Error propagated through get/post/put/delete; api_test.go
covers same-host extraction, query preservation, and foreign-host rejection.
API-controlled strings (names, summaries, notices, headlines, affordance
commands) reached styled/markdown sinks and the --watch TUI without ANSI
stripping, allowing terminal/OSC injection. Sanitize WithSummary/WithNotice/
WithDiagnostic at the source, the timeline watch TUI fields (creator name
stripped before the empty check so an all-escape name still falls back to a
placeholder), and presenter format helpers, with defense-in-depth at the
render sinks. Stripping is centralized in RenderTemplate + RenderHeadline so
schema headlines ({{.name}}/{{.subject}}/{{.content}}) and affordance commands
are all covered, including the identity-label headline fallback.
Assert the exact sanitized string instead of mirroring ansi.Strip,
which is a weaker contract than richtext.SanitizeTerminal and ties
the expectation to the third-party stripper's behavior.
Stripping keys let a hostile key like "\x1b[31mtitle" collapse onto a
legitimate "title", silently replacing or hiding that field in TTY
--jq output. Keys changed by sanitization are now visibly escaped via
strconv.Quote, so clean keys always keep their slot and no entry is
dropped. Also update a stale test comment that referenced ansi.Strip.
The escaped form of a hostile key could still collide with a literal
key that mimics escape notation (e.g. a real ESC-prefixed title vs the
text "\x1b[31mtitle"), nondeterministically dropping one entry. Clean
keys now claim their names first, and escaped keys re-quote with
delimiters until unique, in sorted order for deterministic output.

Copilot AI left a comment

Copy link
Copy Markdown

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 12 out of 12 changed files in this pull request and generated 1 comment.

Comment thread internal/output/envelope.go

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0097ae3063

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/output/envelope.go Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 59baa57217

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/output/envelope.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown

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 12 out of 12 changed files in this pull request and generated 1 comment.

Comment thread internal/output/envelope.go

Copilot AI left a comment

Copy link
Copy Markdown

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 12 out of 12 changed files in this pull request and generated 3 comments.

Comment thread internal/presenter/template.go Outdated
Comment thread internal/presenter/render.go Outdated
Comment thread internal/presenter/render.go Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

status = errStyle.Render(fmt.Sprintf("Error: %s", m.err))

P2 Badge Sanitize watch-mode error text

When basecamp timeline --watch is polling and m.fetchFn returns an SDK/API error, m.err can contain server-controlled text, but this status line renders it directly in the alt-screen TUI. The new sanitization covers event fields and the person/project description, yet a failing API response containing OSC/CSI/C1 controls would still reach the terminal here; sanitize the error string before passing it to errStyle.Render.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ef341d4574

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/output/render.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown

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 12 out of 12 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (2)

internal/output/envelope.go:685

  • In schema-aware styled output, Summary/Notice rendering is gated on the raw resp.Summary/resp.Notice but the sanitized value can become empty (all-escape/control input). That can currently produce blank header lines and still insert the extra spacer newline. Consider checking the sanitized values and only emitting/spacing when non-empty.
	if resp.Summary != "" {
		out.WriteString(r.Summary.Render(sanitizeText(resp.Summary, true, false)))
		out.WriteString("\n")
	}

	if resp.Notice != "" {
		out.WriteString(r.Hint.Render(sanitizeText(resp.Notice, true, false)))
		out.WriteString("\n")
	}

	if resp.Summary != "" || resp.Notice != "" {
		out.WriteString("\n")
	}

internal/output/envelope.go:743

  • In schema-aware Markdown output, Summary/Notice rendering is gated on the raw resp.Summary/resp.Notice but the sanitized value can become empty (e.g. all-escape/control). That can emit malformed markdown like a blank ## heading or ** line. Use the sanitized value for the check and only add the spacer newline when something was actually written.
	// Sink-level ANSI stripping (see presentStyledEntity).
	if resp.Summary != "" {
		out.WriteString("## " + sanitizeText(resp.Summary, true, false) + "\n")
	}

	if resp.Notice != "" {
		out.WriteString("*" + sanitizeText(resp.Notice, true, false) + "*\n")
	}

	if resp.Summary != "" || resp.Notice != "" {
		out.WriteString("\n")
	}

Comment thread internal/output/render.go Outdated
Comment thread internal/output/render.go Outdated
Comment thread internal/output/envelope.go
Comment thread internal/commands/timeline.go Outdated
Comment thread internal/commands/timeline.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown

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 12 out of 12 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

internal/presenter/format.go:185

  • parseDockItems sanitizes dock item title/name with SanitizeTerminal, which preserves \n/\t. Dock lines are rendered as a line-oriented listing, so API-controlled dock strings containing newlines/tabs can still break the per-item layout or inject extra lines. Use SanitizeSingleLine for title/name so each dock item stays on one line after sanitization.
	result := make([]dockItem, len(items))
	for i, m := range items {
		title, _ := m["title"].(string)
		title = richtext.SanitizeTerminal(title)
		name, _ := m["name"].(string)
		name = richtext.SanitizeTerminal(name)
		if title == "" {
			title = name
		}
		disabled := false

Comment thread internal/presenter/format.go
Comment thread internal/presenter/format.go

Copilot AI left a comment

Copy link
Copy Markdown

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 12 out of 12 changed files in this pull request and generated 1 comment.

Comment thread internal/output/output_test.go Outdated
Request IDs and string ids print to line-oriented sinks, but
SanitizeTerminal preserves newlines/tabs (which inject lines or
columns into ids output) and deletes bare CR (which joins adjacent
words). A shared sanitizeSingleLine helper normalizes CR/CRLF to
newlines first, then strips and collapses all whitespace to single
spaces.

Copilot AI left a comment

Copy link
Copy Markdown

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 12 out of 12 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working commands CLI command implementations output Output formatting and presentation tests Tests (unit and e2e)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants