Mask JSON bodies served as application/octet-stream - #104

Merged
brandonc merged 1 commit into
hashicorp:mainfrom
jordanenglish:fix/mask-octet-stream-json-output
Aug 31, 2026
Merged

Mask JSON bodies served as application/octet-stream#104
brandonc merged 1 commit into
hashicorp:mainfrom
jordanenglish:fix/mask-octet-stream-json-output

Conversation

@jordanenglish

Copy link
Copy Markdown
Contributor

Description

Follow-up to #101. CopyRaw only attempted to parse and mask a raw response body when its Content-Type was application/json (or ended in +json). At least one Terraform Enterprise endpoint (the plan JSON export) serves valid JSON labeled application/octet-stream instead, so that response bypassed masking entirely.

application/octet-stream isn't treated as an unconditional mask candidate, since this API also uses that label for genuinely binary or large bodies elsewhere (state archives, plan/apply logs fetched via signed archivist URLs), and buffering one of those just because a sibling endpoint is mislabeled would trade a confirmed small leak for a real cost on unrelated responses. Instead, a body labeled application/octet-stream is peeked at, without consuming or buffering it, to check whether its first non-whitespace byte opens a JSON object or array. Only then does it proceed to the existing buffer-and-mask path; otherwise it streams through unread, exactly as before #101.

Tests

  • internal/pkg/format/redact_test.go, TestCopyRaw: extended with five new subtests covering the application/octet-stream branch specifically:
    • masks a JSON body labeled application/octet-stream, the reported bug
    • masks the same case with leading whitespace before the opening brace, exercising the peek's whitespace-skip
    • passes a genuinely binary application/octet-stream body through unread (leading bytes that aren't {/[)
    • passes through, unmasked, an application/octet-stream body shaped like a real plan/apply log: human-readable text followed by embedded JSON lines, mirroring the exact shape already covered by TestRunAPI_GetArbitraryURL in internal/commands/api. This is the regression case that motivated the peek approach over a blanket Content-Type change: that existing test's fixture is real evidence this API serves non-JSON content under application/octet-stream, and it must not start getting buffered as a side effect of this fix
    • confirms a body under an unrelated content type (text/plain) still isn't touched, out of scope for this fix
  • go test ./...: full suite passes, including internal/commands/api (home of the log-shaped octet-stream fixture above) and internal/commands/run
  • gofmt and go vet ./...: clean
  • Manually verified against a Terraform Enterprise instance where the plan JSON export endpoint was confirmed (via --debug) to serve Content-Type: application/octet-stream for a body containing several sensitive = true Terraform variables, including one PEM-format private key and one token matching the GitHub token shape rule. Before this fix, all of them rendered in cleartext through tfctl api .../json-output despite Redact sensitive values from command output #101. After, all mask to (redacted) with the usual WARNING: masked N sensitive fields report, and an ordinary structured tfctl get call against the same instance is unaffected

PR Checklist

  • Run npx changie new or install changie to prepare a new changelog entry for the next set of release notes.
  • Ensure any command changes are sensitive to these global flags:
    • No command-level changes, global-flags checklist doesn't apply
  • Get the logging interface from the context and add debug logging for interesting conditions and nonfatal situations.
  • Run make gen/screenshot if the root command output changes.
    • No root command output change, no screenshot regen needed
  • Add the Autocomplete field to positional arguments and flags to assist shell autocomplete.
    • No new flags/arguments, no Autocomplete field needed

PCI review checklist

  • I have documented a clear reason for, and description of, the change I am making.

  • If applicable, I've documented a plan to revert these changes if they require more than reverting the pull request.

    • Reverting fully removes the change; no migration.
  • If applicable, I've documented the impact of any changes to security controls.

CopyRaw, added in hashicorp#101, only attempted to parse and mask a raw
response body when its Content-Type was application/json (or ended
in +json). At least one Terraform Enterprise endpoint (the plan JSON
export) serves valid JSON labeled application/octet-stream instead,
so that response bypassed masking entirely and streamed straight
through.
application/octet-stream is not treated as an unconditional mask
candidate, since this API also uses it for genuinely binary or large
bodies elsewhere (state archives, plan/apply logs fetched via signed
archivist URLs), and buffering one of those into memory just because
a sibling endpoint is mislabeled would trade a confirmed small leak
for a real cost on unrelated responses. Instead, a body labeled
application/octet-stream is peeked at, without consuming or buffering
it, to check whether its first non-whitespace byte opens a JSON
object or array. Only then does it proceed to the existing
buffer-and-mask path; otherwise it streams through unread, exactly as
it did before hashicorp#101.
@@ -0,0 +1,3 @@
kind: BUG FIXES

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since this feature has not been released yet, we can drop the bug fix entry 😎

@brandonc
brandonc merged commit ae5c136 into hashicorp:mainAug 31, 2026
3 checks passed
Sign up for freeto 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.

2 participants

@jordanenglish@brandonc
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Mask JSON bodies served as application/octet-stream - #104

Merged
brandonc merged 1 commit into
hashicorp:mainfrom
jordanenglish:fix/mask-octet-stream-json-output
Aug 31, 2026
Merged

Mask JSON bodies served as application/octet-stream#104
brandonc merged 1 commit into
hashicorp:mainfrom
jordanenglish:fix/mask-octet-stream-json-output

Conversation

@jordanenglish

Copy link
Copy Markdown
Contributor

Description

Follow-up to #101. CopyRaw only attempted to parse and mask a raw response body when its Content-Type was application/json (or ended in +json). At least one Terraform Enterprise endpoint (the plan JSON export) serves valid JSON labeled application/octet-stream instead, so that response bypassed masking entirely.

application/octet-stream isn't treated as an unconditional mask candidate, since this API also uses that label for genuinely binary or large bodies elsewhere (state archives, plan/apply logs fetched via signed archivist URLs), and buffering one of those just because a sibling endpoint is mislabeled would trade a confirmed small leak for a real cost on unrelated responses. Instead, a body labeled application/octet-stream is peeked at, without consuming or buffering it, to check whether its first non-whitespace byte opens a JSON object or array. Only then does it proceed to the existing buffer-and-mask path; otherwise it streams through unread, exactly as before #101.

Tests

  • internal/pkg/format/redact_test.go, TestCopyRaw: extended with five new subtests covering the application/octet-stream branch specifically:
    • masks a JSON body labeled application/octet-stream, the reported bug
    • masks the same case with leading whitespace before the opening brace, exercising the peek's whitespace-skip
    • passes a genuinely binary application/octet-stream body through unread (leading bytes that aren't {/[)
    • passes through, unmasked, an application/octet-stream body shaped like a real plan/apply log: human-readable text followed by embedded JSON lines, mirroring the exact shape already covered by TestRunAPI_GetArbitraryURL in internal/commands/api. This is the regression case that motivated the peek approach over a blanket Content-Type change: that existing test's fixture is real evidence this API serves non-JSON content under application/octet-stream, and it must not start getting buffered as a side effect of this fix
    • confirms a body under an unrelated content type (text/plain) still isn't touched, out of scope for this fix
  • go test ./...: full suite passes, including internal/commands/api (home of the log-shaped octet-stream fixture above) and internal/commands/run
  • gofmt and go vet ./...: clean
  • Manually verified against a Terraform Enterprise instance where the plan JSON export endpoint was confirmed (via --debug) to serve Content-Type: application/octet-stream for a body containing several sensitive = true Terraform variables, including one PEM-format private key and one token matching the GitHub token shape rule. Before this fix, all of them rendered in cleartext through tfctl api .../json-output despite Redact sensitive values from command output #101. After, all mask to (redacted) with the usual WARNING: masked N sensitive fields report, and an ordinary structured tfctl get call against the same instance is unaffected

PR Checklist

  • Run npx changie new or install changie to prepare a new changelog entry for the next set of release notes.
  • Ensure any command changes are sensitive to these global flags:
    • No command-level changes, global-flags checklist doesn't apply
  • Get the logging interface from the context and add debug logging for interesting conditions and nonfatal situations.
  • Run make gen/screenshot if the root command output changes.
    • No root command output change, no screenshot regen needed
  • Add the Autocomplete field to positional arguments and flags to assist shell autocomplete.
    • No new flags/arguments, no Autocomplete field needed

PCI review checklist

  • I have documented a clear reason for, and description of, the change I am making.

  • If applicable, I've documented a plan to revert these changes if they require more than reverting the pull request.

    • Reverting fully removes the change; no migration.
  • If applicable, I've documented the impact of any changes to security controls.

CopyRaw, added in hashicorp#101, only attempted to parse and mask a raw
response body when its Content-Type was application/json (or ended
in +json). At least one Terraform Enterprise endpoint (the plan JSON
export) serves valid JSON labeled application/octet-stream instead,
so that response bypassed masking entirely and streamed straight
through.
application/octet-stream is not treated as an unconditional mask
candidate, since this API also uses it for genuinely binary or large
bodies elsewhere (state archives, plan/apply logs fetched via signed
archivist URLs), and buffering one of those into memory just because
a sibling endpoint is mislabeled would trade a confirmed small leak
for a real cost on unrelated responses. Instead, a body labeled
application/octet-stream is peeked at, without consuming or buffering
it, to check whether its first non-whitespace byte opens a JSON
object or array. Only then does it proceed to the existing
buffer-and-mask path; otherwise it streams through unread, exactly as
it did before hashicorp#101.
@@ -0,0 +1,3 @@
kind: BUG FIXES

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since this feature has not been released yet, we can drop the bug fix entry 😎

@brandonc
brandonc merged commit ae5c136 into hashicorp:mainAug 31, 2026
3 checks passed
Sign up for freeto 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.

2 participants

@jordanenglish@brandonc
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Mask JSON bodies served as application/octet-stream - #104

Merged
brandonc merged 1 commit into
hashicorp:mainfrom
jordanenglish:fix/mask-octet-stream-json-output
Aug 31, 2026
Merged

Mask JSON bodies served as application/octet-stream#104
brandonc merged 1 commit into
hashicorp:mainfrom
jordanenglish:fix/mask-octet-stream-json-output

Conversation

@jordanenglish

Copy link
Copy Markdown
Contributor

Description

Follow-up to #101. CopyRaw only attempted to parse and mask a raw response body when its Content-Type was application/json (or ended in +json). At least one Terraform Enterprise endpoint (the plan JSON export) serves valid JSON labeled application/octet-stream instead, so that response bypassed masking entirely.

application/octet-stream isn't treated as an unconditional mask candidate, since this API also uses that label for genuinely binary or large bodies elsewhere (state archives, plan/apply logs fetched via signed archivist URLs), and buffering one of those just because a sibling endpoint is mislabeled would trade a confirmed small leak for a real cost on unrelated responses. Instead, a body labeled application/octet-stream is peeked at, without consuming or buffering it, to check whether its first non-whitespace byte opens a JSON object or array. Only then does it proceed to the existing buffer-and-mask path; otherwise it streams through unread, exactly as before #101.

Tests

  • internal/pkg/format/redact_test.go, TestCopyRaw: extended with five new subtests covering the application/octet-stream branch specifically:
    • masks a JSON body labeled application/octet-stream, the reported bug
    • masks the same case with leading whitespace before the opening brace, exercising the peek's whitespace-skip
    • passes a genuinely binary application/octet-stream body through unread (leading bytes that aren't {/[)
    • passes through, unmasked, an application/octet-stream body shaped like a real plan/apply log: human-readable text followed by embedded JSON lines, mirroring the exact shape already covered by TestRunAPI_GetArbitraryURL in internal/commands/api. This is the regression case that motivated the peek approach over a blanket Content-Type change: that existing test's fixture is real evidence this API serves non-JSON content under application/octet-stream, and it must not start getting buffered as a side effect of this fix
    • confirms a body under an unrelated content type (text/plain) still isn't touched, out of scope for this fix
  • go test ./...: full suite passes, including internal/commands/api (home of the log-shaped octet-stream fixture above) and internal/commands/run
  • gofmt and go vet ./...: clean
  • Manually verified against a Terraform Enterprise instance where the plan JSON export endpoint was confirmed (via --debug) to serve Content-Type: application/octet-stream for a body containing several sensitive = true Terraform variables, including one PEM-format private key and one token matching the GitHub token shape rule. Before this fix, all of them rendered in cleartext through tfctl api .../json-output despite Redact sensitive values from command output #101. After, all mask to (redacted) with the usual WARNING: masked N sensitive fields report, and an ordinary structured tfctl get call against the same instance is unaffected

PR Checklist

  • Run npx changie new or install changie to prepare a new changelog entry for the next set of release notes.
  • Ensure any command changes are sensitive to these global flags:
    • No command-level changes, global-flags checklist doesn't apply
  • Get the logging interface from the context and add debug logging for interesting conditions and nonfatal situations.
  • Run make gen/screenshot if the root command output changes.
    • No root command output change, no screenshot regen needed
  • Add the Autocomplete field to positional arguments and flags to assist shell autocomplete.
    • No new flags/arguments, no Autocomplete field needed

PCI review checklist

  • I have documented a clear reason for, and description of, the change I am making.

  • If applicable, I've documented a plan to revert these changes if they require more than reverting the pull request.

    • Reverting fully removes the change; no migration.
  • If applicable, I've documented the impact of any changes to security controls.

CopyRaw, added in hashicorp#101, only attempted to parse and mask a raw
response body when its Content-Type was application/json (or ended
in +json). At least one Terraform Enterprise endpoint (the plan JSON
export) serves valid JSON labeled application/octet-stream instead,
so that response bypassed masking entirely and streamed straight
through.
application/octet-stream is not treated as an unconditional mask
candidate, since this API also uses it for genuinely binary or large
bodies elsewhere (state archives, plan/apply logs fetched via signed
archivist URLs), and buffering one of those into memory just because
a sibling endpoint is mislabeled would trade a confirmed small leak
for a real cost on unrelated responses. Instead, a body labeled
application/octet-stream is peeked at, without consuming or buffering
it, to check whether its first non-whitespace byte opens a JSON
object or array. Only then does it proceed to the existing
buffer-and-mask path; otherwise it streams through unread, exactly as
it did before hashicorp#101.
@@ -0,0 +1,3 @@
kind: BUG FIXES

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since this feature has not been released yet, we can drop the bug fix entry 😎

@brandonc
brandonc merged commit ae5c136 into hashicorp:mainAug 31, 2026
3 checks passed
Sign up for freeto 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.

2 participants

@jordanenglish@brandonc
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Mask JSON bodies served as application/octet-stream - #104

Merged
brandonc merged 1 commit into
hashicorp:mainfrom
jordanenglish:fix/mask-octet-stream-json-output
Aug 31, 2026
Merged

Mask JSON bodies served as application/octet-stream#104
brandonc merged 1 commit into
hashicorp:mainfrom
jordanenglish:fix/mask-octet-stream-json-output

Conversation

@jordanenglish

Copy link
Copy Markdown
Contributor

Description

Follow-up to #101. CopyRaw only attempted to parse and mask a raw response body when its Content-Type was application/json (or ended in +json). At least one Terraform Enterprise endpoint (the plan JSON export) serves valid JSON labeled application/octet-stream instead, so that response bypassed masking entirely.

application/octet-stream isn't treated as an unconditional mask candidate, since this API also uses that label for genuinely binary or large bodies elsewhere (state archives, plan/apply logs fetched via signed archivist URLs), and buffering one of those just because a sibling endpoint is mislabeled would trade a confirmed small leak for a real cost on unrelated responses. Instead, a body labeled application/octet-stream is peeked at, without consuming or buffering it, to check whether its first non-whitespace byte opens a JSON object or array. Only then does it proceed to the existing buffer-and-mask path; otherwise it streams through unread, exactly as before #101.

Tests

  • internal/pkg/format/redact_test.go, TestCopyRaw: extended with five new subtests covering the application/octet-stream branch specifically:
    • masks a JSON body labeled application/octet-stream, the reported bug
    • masks the same case with leading whitespace before the opening brace, exercising the peek's whitespace-skip
    • passes a genuinely binary application/octet-stream body through unread (leading bytes that aren't {/[)
    • passes through, unmasked, an application/octet-stream body shaped like a real plan/apply log: human-readable text followed by embedded JSON lines, mirroring the exact shape already covered by TestRunAPI_GetArbitraryURL in internal/commands/api. This is the regression case that motivated the peek approach over a blanket Content-Type change: that existing test's fixture is real evidence this API serves non-JSON content under application/octet-stream, and it must not start getting buffered as a side effect of this fix
    • confirms a body under an unrelated content type (text/plain) still isn't touched, out of scope for this fix
  • go test ./...: full suite passes, including internal/commands/api (home of the log-shaped octet-stream fixture above) and internal/commands/run
  • gofmt and go vet ./...: clean
  • Manually verified against a Terraform Enterprise instance where the plan JSON export endpoint was confirmed (via --debug) to serve Content-Type: application/octet-stream for a body containing several sensitive = true Terraform variables, including one PEM-format private key and one token matching the GitHub token shape rule. Before this fix, all of them rendered in cleartext through tfctl api .../json-output despite Redact sensitive values from command output #101. After, all mask to (redacted) with the usual WARNING: masked N sensitive fields report, and an ordinary structured tfctl get call against the same instance is unaffected

PR Checklist

  • Run npx changie new or install changie to prepare a new changelog entry for the next set of release notes.
  • Ensure any command changes are sensitive to these global flags:
    • No command-level changes, global-flags checklist doesn't apply
  • Get the logging interface from the context and add debug logging for interesting conditions and nonfatal situations.
  • Run make gen/screenshot if the root command output changes.
    • No root command output change, no screenshot regen needed
  • Add the Autocomplete field to positional arguments and flags to assist shell autocomplete.
    • No new flags/arguments, no Autocomplete field needed

PCI review checklist

  • I have documented a clear reason for, and description of, the change I am making.

  • If applicable, I've documented a plan to revert these changes if they require more than reverting the pull request.

    • Reverting fully removes the change; no migration.
  • If applicable, I've documented the impact of any changes to security controls.

CopyRaw, added in hashicorp#101, only attempted to parse and mask a raw
response body when its Content-Type was application/json (or ended
in +json). At least one Terraform Enterprise endpoint (the plan JSON
export) serves valid JSON labeled application/octet-stream instead,
so that response bypassed masking entirely and streamed straight
through.
application/octet-stream is not treated as an unconditional mask
candidate, since this API also uses it for genuinely binary or large
bodies elsewhere (state archives, plan/apply logs fetched via signed
archivist URLs), and buffering one of those into memory just because
a sibling endpoint is mislabeled would trade a confirmed small leak
for a real cost on unrelated responses. Instead, a body labeled
application/octet-stream is peeked at, without consuming or buffering
it, to check whether its first non-whitespace byte opens a JSON
object or array. Only then does it proceed to the existing
buffer-and-mask path; otherwise it streams through unread, exactly as
it did before hashicorp#101.
@@ -0,0 +1,3 @@
kind: BUG FIXES

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since this feature has not been released yet, we can drop the bug fix entry 😎

@brandonc
brandonc merged commit ae5c136 into hashicorp:mainAug 31, 2026
3 checks passed
Sign up for freeto 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.

2 participants

@jordanenglish@brandonc
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Mask JSON bodies served as application/octet-stream - #104

Merged
brandonc merged 1 commit into
hashicorp:mainfrom
jordanenglish:fix/mask-octet-stream-json-output
Aug 31, 2026
Merged

Mask JSON bodies served as application/octet-stream#104
brandonc merged 1 commit into
hashicorp:mainfrom
jordanenglish:fix/mask-octet-stream-json-output

Conversation

@jordanenglish

Copy link
Copy Markdown
Contributor

Description

Follow-up to #101. CopyRaw only attempted to parse and mask a raw response body when its Content-Type was application/json (or ended in +json). At least one Terraform Enterprise endpoint (the plan JSON export) serves valid JSON labeled application/octet-stream instead, so that response bypassed masking entirely.

application/octet-stream isn't treated as an unconditional mask candidate, since this API also uses that label for genuinely binary or large bodies elsewhere (state archives, plan/apply logs fetched via signed archivist URLs), and buffering one of those just because a sibling endpoint is mislabeled would trade a confirmed small leak for a real cost on unrelated responses. Instead, a body labeled application/octet-stream is peeked at, without consuming or buffering it, to check whether its first non-whitespace byte opens a JSON object or array. Only then does it proceed to the existing buffer-and-mask path; otherwise it streams through unread, exactly as before #101.

Tests

  • internal/pkg/format/redact_test.go, TestCopyRaw: extended with five new subtests covering the application/octet-stream branch specifically:
    • masks a JSON body labeled application/octet-stream, the reported bug
    • masks the same case with leading whitespace before the opening brace, exercising the peek's whitespace-skip
    • passes a genuinely binary application/octet-stream body through unread (leading bytes that aren't {/[)
    • passes through, unmasked, an application/octet-stream body shaped like a real plan/apply log: human-readable text followed by embedded JSON lines, mirroring the exact shape already covered by TestRunAPI_GetArbitraryURL in internal/commands/api. This is the regression case that motivated the peek approach over a blanket Content-Type change: that existing test's fixture is real evidence this API serves non-JSON content under application/octet-stream, and it must not start getting buffered as a side effect of this fix
    • confirms a body under an unrelated content type (text/plain) still isn't touched, out of scope for this fix
  • go test ./...: full suite passes, including internal/commands/api (home of the log-shaped octet-stream fixture above) and internal/commands/run
  • gofmt and go vet ./...: clean
  • Manually verified against a Terraform Enterprise instance where the plan JSON export endpoint was confirmed (via --debug) to serve Content-Type: application/octet-stream for a body containing several sensitive = true Terraform variables, including one PEM-format private key and one token matching the GitHub token shape rule. Before this fix, all of them rendered in cleartext through tfctl api .../json-output despite Redact sensitive values from command output #101. After, all mask to (redacted) with the usual WARNING: masked N sensitive fields report, and an ordinary structured tfctl get call against the same instance is unaffected

PR Checklist

  • Run npx changie new or install changie to prepare a new changelog entry for the next set of release notes.
  • Ensure any command changes are sensitive to these global flags:
    • No command-level changes, global-flags checklist doesn't apply
  • Get the logging interface from the context and add debug logging for interesting conditions and nonfatal situations.
  • Run make gen/screenshot if the root command output changes.
    • No root command output change, no screenshot regen needed
  • Add the Autocomplete field to positional arguments and flags to assist shell autocomplete.
    • No new flags/arguments, no Autocomplete field needed

PCI review checklist

  • I have documented a clear reason for, and description of, the change I am making.

  • If applicable, I've documented a plan to revert these changes if they require more than reverting the pull request.

    • Reverting fully removes the change; no migration.
  • If applicable, I've documented the impact of any changes to security controls.

CopyRaw, added in hashicorp#101, only attempted to parse and mask a raw
response body when its Content-Type was application/json (or ended
in +json). At least one Terraform Enterprise endpoint (the plan JSON
export) serves valid JSON labeled application/octet-stream instead,
so that response bypassed masking entirely and streamed straight
through.
application/octet-stream is not treated as an unconditional mask
candidate, since this API also uses it for genuinely binary or large
bodies elsewhere (state archives, plan/apply logs fetched via signed
archivist URLs), and buffering one of those into memory just because
a sibling endpoint is mislabeled would trade a confirmed small leak
for a real cost on unrelated responses. Instead, a body labeled
application/octet-stream is peeked at, without consuming or buffering
it, to check whether its first non-whitespace byte opens a JSON
object or array. Only then does it proceed to the existing
buffer-and-mask path; otherwise it streams through unread, exactly as
it did before hashicorp#101.
@@ -0,0 +1,3 @@
kind: BUG FIXES

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since this feature has not been released yet, we can drop the bug fix entry 😎

@brandonc
brandonc merged commit ae5c136 into hashicorp:mainAug 31, 2026
3 checks passed
Sign up for freeto 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.

2 participants

@jordanenglish@brandonc
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Mask JSON bodies served as application/octet-stream - #104

Merged
brandonc merged 1 commit into
hashicorp:mainfrom
jordanenglish:fix/mask-octet-stream-json-output
Aug 31, 2026
Merged

Mask JSON bodies served as application/octet-stream#104
brandonc merged 1 commit into
hashicorp:mainfrom
jordanenglish:fix/mask-octet-stream-json-output

Conversation

@jordanenglish

Copy link
Copy Markdown
Contributor

Description

Follow-up to #101. CopyRaw only attempted to parse and mask a raw response body when its Content-Type was application/json (or ended in +json). At least one Terraform Enterprise endpoint (the plan JSON export) serves valid JSON labeled application/octet-stream instead, so that response bypassed masking entirely.

application/octet-stream isn't treated as an unconditional mask candidate, since this API also uses that label for genuinely binary or large bodies elsewhere (state archives, plan/apply logs fetched via signed archivist URLs), and buffering one of those just because a sibling endpoint is mislabeled would trade a confirmed small leak for a real cost on unrelated responses. Instead, a body labeled application/octet-stream is peeked at, without consuming or buffering it, to check whether its first non-whitespace byte opens a JSON object or array. Only then does it proceed to the existing buffer-and-mask path; otherwise it streams through unread, exactly as before #101.

Tests

  • internal/pkg/format/redact_test.go, TestCopyRaw: extended with five new subtests covering the application/octet-stream branch specifically:
    • masks a JSON body labeled application/octet-stream, the reported bug
    • masks the same case with leading whitespace before the opening brace, exercising the peek's whitespace-skip
    • passes a genuinely binary application/octet-stream body through unread (leading bytes that aren't {/[)
    • passes through, unmasked, an application/octet-stream body shaped like a real plan/apply log: human-readable text followed by embedded JSON lines, mirroring the exact shape already covered by TestRunAPI_GetArbitraryURL in internal/commands/api. This is the regression case that motivated the peek approach over a blanket Content-Type change: that existing test's fixture is real evidence this API serves non-JSON content under application/octet-stream, and it must not start getting buffered as a side effect of this fix
    • confirms a body under an unrelated content type (text/plain) still isn't touched, out of scope for this fix
  • go test ./...: full suite passes, including internal/commands/api (home of the log-shaped octet-stream fixture above) and internal/commands/run
  • gofmt and go vet ./...: clean
  • Manually verified against a Terraform Enterprise instance where the plan JSON export endpoint was confirmed (via --debug) to serve Content-Type: application/octet-stream for a body containing several sensitive = true Terraform variables, including one PEM-format private key and one token matching the GitHub token shape rule. Before this fix, all of them rendered in cleartext through tfctl api .../json-output despite Redact sensitive values from command output #101. After, all mask to (redacted) with the usual WARNING: masked N sensitive fields report, and an ordinary structured tfctl get call against the same instance is unaffected

PR Checklist

  • Run npx changie new or install changie to prepare a new changelog entry for the next set of release notes.
  • Ensure any command changes are sensitive to these global flags:
    • No command-level changes, global-flags checklist doesn't apply
  • Get the logging interface from the context and add debug logging for interesting conditions and nonfatal situations.
  • Run make gen/screenshot if the root command output changes.
    • No root command output change, no screenshot regen needed
  • Add the Autocomplete field to positional arguments and flags to assist shell autocomplete.
    • No new flags/arguments, no Autocomplete field needed

PCI review checklist

  • I have documented a clear reason for, and description of, the change I am making.

  • If applicable, I've documented a plan to revert these changes if they require more than reverting the pull request.

    • Reverting fully removes the change; no migration.
  • If applicable, I've documented the impact of any changes to security controls.

CopyRaw, added in hashicorp#101, only attempted to parse and mask a raw
response body when its Content-Type was application/json (or ended
in +json). At least one Terraform Enterprise endpoint (the plan JSON
export) serves valid JSON labeled application/octet-stream instead,
so that response bypassed masking entirely and streamed straight
through.
application/octet-stream is not treated as an unconditional mask
candidate, since this API also uses it for genuinely binary or large
bodies elsewhere (state archives, plan/apply logs fetched via signed
archivist URLs), and buffering one of those into memory just because
a sibling endpoint is mislabeled would trade a confirmed small leak
for a real cost on unrelated responses. Instead, a body labeled
application/octet-stream is peeked at, without consuming or buffering
it, to check whether its first non-whitespace byte opens a JSON
object or array. Only then does it proceed to the existing
buffer-and-mask path; otherwise it streams through unread, exactly as
it did before hashicorp#101.
@@ -0,0 +1,3 @@
kind: BUG FIXES

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since this feature has not been released yet, we can drop the bug fix entry 😎

@brandonc
brandonc merged commit ae5c136 into hashicorp:mainAug 31, 2026
3 checks passed
Sign up for freeto 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.

2 participants

@jordanenglish@brandonc
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Mask JSON bodies served as application/octet-stream - #104

Merged
brandonc merged 1 commit into
hashicorp:mainfrom
jordanenglish:fix/mask-octet-stream-json-output
Aug 31, 2026
Merged

Mask JSON bodies served as application/octet-stream#104
brandonc merged 1 commit into
hashicorp:mainfrom
jordanenglish:fix/mask-octet-stream-json-output

Conversation

@jordanenglish

Copy link
Copy Markdown
Contributor

Description

Follow-up to #101. CopyRaw only attempted to parse and mask a raw response body when its Content-Type was application/json (or ended in +json). At least one Terraform Enterprise endpoint (the plan JSON export) serves valid JSON labeled application/octet-stream instead, so that response bypassed masking entirely.

application/octet-stream isn't treated as an unconditional mask candidate, since this API also uses that label for genuinely binary or large bodies elsewhere (state archives, plan/apply logs fetched via signed archivist URLs), and buffering one of those just because a sibling endpoint is mislabeled would trade a confirmed small leak for a real cost on unrelated responses. Instead, a body labeled application/octet-stream is peeked at, without consuming or buffering it, to check whether its first non-whitespace byte opens a JSON object or array. Only then does it proceed to the existing buffer-and-mask path; otherwise it streams through unread, exactly as before #101.

Tests

  • internal/pkg/format/redact_test.go, TestCopyRaw: extended with five new subtests covering the application/octet-stream branch specifically:
    • masks a JSON body labeled application/octet-stream, the reported bug
    • masks the same case with leading whitespace before the opening brace, exercising the peek's whitespace-skip
    • passes a genuinely binary application/octet-stream body through unread (leading bytes that aren't {/[)
    • passes through, unmasked, an application/octet-stream body shaped like a real plan/apply log: human-readable text followed by embedded JSON lines, mirroring the exact shape already covered by TestRunAPI_GetArbitraryURL in internal/commands/api. This is the regression case that motivated the peek approach over a blanket Content-Type change: that existing test's fixture is real evidence this API serves non-JSON content under application/octet-stream, and it must not start getting buffered as a side effect of this fix
    • confirms a body under an unrelated content type (text/plain) still isn't touched, out of scope for this fix
  • go test ./...: full suite passes, including internal/commands/api (home of the log-shaped octet-stream fixture above) and internal/commands/run
  • gofmt and go vet ./...: clean
  • Manually verified against a Terraform Enterprise instance where the plan JSON export endpoint was confirmed (via --debug) to serve Content-Type: application/octet-stream for a body containing several sensitive = true Terraform variables, including one PEM-format private key and one token matching the GitHub token shape rule. Before this fix, all of them rendered in cleartext through tfctl api .../json-output despite Redact sensitive values from command output #101. After, all mask to (redacted) with the usual WARNING: masked N sensitive fields report, and an ordinary structured tfctl get call against the same instance is unaffected

PR Checklist

  • Run npx changie new or install changie to prepare a new changelog entry for the next set of release notes.
  • Ensure any command changes are sensitive to these global flags:
    • No command-level changes, global-flags checklist doesn't apply
  • Get the logging interface from the context and add debug logging for interesting conditions and nonfatal situations.
  • Run make gen/screenshot if the root command output changes.
    • No root command output change, no screenshot regen needed
  • Add the Autocomplete field to positional arguments and flags to assist shell autocomplete.
    • No new flags/arguments, no Autocomplete field needed

PCI review checklist

  • I have documented a clear reason for, and description of, the change I am making.

  • If applicable, I've documented a plan to revert these changes if they require more than reverting the pull request.

    • Reverting fully removes the change; no migration.
  • If applicable, I've documented the impact of any changes to security controls.

CopyRaw, added in hashicorp#101, only attempted to parse and mask a raw
response body when its Content-Type was application/json (or ended
in +json). At least one Terraform Enterprise endpoint (the plan JSON
export) serves valid JSON labeled application/octet-stream instead,
so that response bypassed masking entirely and streamed straight
through.
application/octet-stream is not treated as an unconditional mask
candidate, since this API also uses it for genuinely binary or large
bodies elsewhere (state archives, plan/apply logs fetched via signed
archivist URLs), and buffering one of those into memory just because
a sibling endpoint is mislabeled would trade a confirmed small leak
for a real cost on unrelated responses. Instead, a body labeled
application/octet-stream is peeked at, without consuming or buffering
it, to check whether its first non-whitespace byte opens a JSON
object or array. Only then does it proceed to the existing
buffer-and-mask path; otherwise it streams through unread, exactly as
it did before hashicorp#101.
@@ -0,0 +1,3 @@
kind: BUG FIXES

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since this feature has not been released yet, we can drop the bug fix entry 😎

@brandonc
brandonc merged commit ae5c136 into hashicorp:mainAug 31, 2026
3 checks passed
Sign up for freeto 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.

2 participants

@jordanenglish@brandonc
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Mask JSON bodies served as application/octet-stream - #104

Merged
brandonc merged 1 commit into
hashicorp:mainfrom
jordanenglish:fix/mask-octet-stream-json-output
Aug 31, 2026
Merged

Mask JSON bodies served as application/octet-stream#104
brandonc merged 1 commit into
hashicorp:mainfrom
jordanenglish:fix/mask-octet-stream-json-output

Conversation

@jordanenglish

Copy link
Copy Markdown
Contributor

Description

Follow-up to #101. CopyRaw only attempted to parse and mask a raw response body when its Content-Type was application/json (or ended in +json). At least one Terraform Enterprise endpoint (the plan JSON export) serves valid JSON labeled application/octet-stream instead, so that response bypassed masking entirely.

application/octet-stream isn't treated as an unconditional mask candidate, since this API also uses that label for genuinely binary or large bodies elsewhere (state archives, plan/apply logs fetched via signed archivist URLs), and buffering one of those just because a sibling endpoint is mislabeled would trade a confirmed small leak for a real cost on unrelated responses. Instead, a body labeled application/octet-stream is peeked at, without consuming or buffering it, to check whether its first non-whitespace byte opens a JSON object or array. Only then does it proceed to the existing buffer-and-mask path; otherwise it streams through unread, exactly as before #101.

Tests

  • internal/pkg/format/redact_test.go, TestCopyRaw: extended with five new subtests covering the application/octet-stream branch specifically:
    • masks a JSON body labeled application/octet-stream, the reported bug
    • masks the same case with leading whitespace before the opening brace, exercising the peek's whitespace-skip
    • passes a genuinely binary application/octet-stream body through unread (leading bytes that aren't {/[)
    • passes through, unmasked, an application/octet-stream body shaped like a real plan/apply log: human-readable text followed by embedded JSON lines, mirroring the exact shape already covered by TestRunAPI_GetArbitraryURL in internal/commands/api. This is the regression case that motivated the peek approach over a blanket Content-Type change: that existing test's fixture is real evidence this API serves non-JSON content under application/octet-stream, and it must not start getting buffered as a side effect of this fix
    • confirms a body under an unrelated content type (text/plain) still isn't touched, out of scope for this fix
  • go test ./...: full suite passes, including internal/commands/api (home of the log-shaped octet-stream fixture above) and internal/commands/run
  • gofmt and go vet ./...: clean
  • Manually verified against a Terraform Enterprise instance where the plan JSON export endpoint was confirmed (via --debug) to serve Content-Type: application/octet-stream for a body containing several sensitive = true Terraform variables, including one PEM-format private key and one token matching the GitHub token shape rule. Before this fix, all of them rendered in cleartext through tfctl api .../json-output despite Redact sensitive values from command output #101. After, all mask to (redacted) with the usual WARNING: masked N sensitive fields report, and an ordinary structured tfctl get call against the same instance is unaffected

PR Checklist

  • Run npx changie new or install changie to prepare a new changelog entry for the next set of release notes.
  • Ensure any command changes are sensitive to these global flags:
    • No command-level changes, global-flags checklist doesn't apply
  • Get the logging interface from the context and add debug logging for interesting conditions and nonfatal situations.
  • Run make gen/screenshot if the root command output changes.
    • No root command output change, no screenshot regen needed
  • Add the Autocomplete field to positional arguments and flags to assist shell autocomplete.
    • No new flags/arguments, no Autocomplete field needed

PCI review checklist

  • I have documented a clear reason for, and description of, the change I am making.

  • If applicable, I've documented a plan to revert these changes if they require more than reverting the pull request.

    • Reverting fully removes the change; no migration.
  • If applicable, I've documented the impact of any changes to security controls.

CopyRaw, added in hashicorp#101, only attempted to parse and mask a raw
response body when its Content-Type was application/json (or ended
in +json). At least one Terraform Enterprise endpoint (the plan JSON
export) serves valid JSON labeled application/octet-stream instead,
so that response bypassed masking entirely and streamed straight
through.
application/octet-stream is not treated as an unconditional mask
candidate, since this API also uses it for genuinely binary or large
bodies elsewhere (state archives, plan/apply logs fetched via signed
archivist URLs), and buffering one of those into memory just because
a sibling endpoint is mislabeled would trade a confirmed small leak
for a real cost on unrelated responses. Instead, a body labeled
application/octet-stream is peeked at, without consuming or buffering
it, to check whether its first non-whitespace byte opens a JSON
object or array. Only then does it proceed to the existing
buffer-and-mask path; otherwise it streams through unread, exactly as
it did before hashicorp#101.
@@ -0,0 +1,3 @@
kind: BUG FIXES

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since this feature has not been released yet, we can drop the bug fix entry 😎

@brandonc
brandonc merged commit ae5c136 into hashicorp:mainAug 31, 2026
3 checks passed
Sign up for freeto 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.

2 participants

@jordanenglish@brandonc