Repository navigation
Keep safe-output token placeholders out of runtime config.json #36353
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
b2d44cb
1131de2
5bbf951
a348be5
cc4ef74
f0cc4b3
c26d768
608e4aa
bf80cee
ea9db61
e301e41
fc95bbe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -257,6 +257,21 @@ function normalizeCommentMemoryId(memoryId, fallback = "default") { | |
| return normalized.length > 0 ? normalized : fallback; | ||
| } | ||
|
|
||
| function redactSensitiveConfig(value) { | ||
| if (Array.isArray(value)) { | ||
| return value.map(redactSensitiveConfig); | ||
| } | ||
| if (value && typeof value === "object") { | ||
| return Object.fromEntries( | ||
| Object.entries(value).map(([key, nestedValue]) => [ | ||
| key, | ||
| /token/i.test(key) ? "***REDACTED***" : redactSensitiveConfig(nestedValue), | ||
| ]), | ||
| ); | ||
| } | ||
| return value; | ||
| } | ||
|
|
||
| /** | ||
| * Load configuration for safe outputs | ||
| * Reads configuration from GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG environment variable | ||
|
|
@@ -269,7 +284,7 @@ function loadConfig() { | |
|
|
||
| try { | ||
| const config = JSON.parse(process.env.GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG); | ||
| core.info(`Loaded config from GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: ${JSON.stringify(config)}`); | ||
| core.info(`Loaded config from GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: ${JSON.stringify(redactSensitiveConfig(config))}`); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] The new 💡 SuggestionAdd a test in
This mirrors the test added in |
||
| // Normalize config keys: convert hyphens to underscores | ||
| return Object.fromEntries(Object.entries(config).map(([k, v]) => [k.replace(/-/g, "_"), v])); | ||
| } catch (error) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,38 @@ const { getErrorMessage } = require("./error_helpers.cjs"); | |
| const fs = require("fs"); | ||
| const path = require("path"); | ||
|
|
||
| function isSensitiveConfigKey(key) { | ||
| return /token/i.test(key); | ||
| } | ||
|
|
||
| function redactSensitiveConfig(value) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/diagnose] 💡 SuggestionExtract to a shared helper module (e.g. |
||
| if (Array.isArray(value)) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Narrow redaction regex leaks non-token sensitive fields into debug logs. Any config key named 💡 Suggested fixBroaden the pattern in function isSensitiveConfigKey(key) {
return /token|secret|key|password|credential|private/i.test(key);
}Without this, a handler config like |
||
| return value.map(redactSensitiveConfig); | ||
| } | ||
| if (value && typeof value === "object") { | ||
| return Object.fromEntries( | ||
| Object.entries(value).map(([key, nestedValue]) => [ | ||
| key, | ||
| isSensitiveConfigKey(key) ? "***REDACTED***" : redactSensitiveConfig(nestedValue), | ||
| ]), | ||
| ); | ||
| } | ||
| return value; | ||
| } | ||
|
|
||
|
|
||
| function resolveEnvPlaceholders(value) { | ||
| if (Array.isArray(value)) { | ||
| return value.map(resolveEnvPlaceholders); | ||
| } | ||
| if (value && typeof value === "object") { | ||
| return Object.fromEntries(Object.entries(value).map(([key, nestedValue]) => [key, resolveEnvPlaceholders(nestedValue)])); | ||
| } | ||
| if (typeof value !== "string") { | ||
| return value; | ||
| } | ||
| return value.replace(/\$\{([A-Z_][A-Z0-9_]*)\}/g, (match, envName) => process.env[envName] ?? match); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/diagnose] The regex 💡 DetailsAll GitHub Actions secret/input env vars generated by the Go compiler ( Either document the uppercase-only contract with a comment, or broaden the regex to return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (match, envName) => process.env[envName] ?? match);
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] The 💡 SuggestionConsider logging a warning (or throwing) when a placeholder cannot be resolved: return value.replace(/\$\{([A-Z_][A-Z0-9_]*)\}/g, (match, envName) => {
if (process.env[envName] === undefined) {
server.warning?.(`Placeholder ${match} has no matching env var — it will be passed literally`);
}
return process.env[envName] ?? match;
});A companion test case for the unresolved-placeholder path would help pin the intended behaviour. |
||
| } | ||
|
|
||
| /** | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unresolved placeholder silently passes through as a literal string, causing confusing downstream failures. When a referenced env var is not set, 💡 Suggested fixEmit a warning (or throw for required fields) when a placeholder remains unresolved: return value.replace(/\$\{([A-Z_][A-Z0-9_]*)\}/g, (match, envName) => {
const resolved = process.env[envName];
if (resolved === undefined) {
// Warn so operators see which variable was missing
server?.warning?.(`Config placeholder ${match} is unset — passing through literally`);
}
return resolved ?? match;
});Without this, a misconfigured workflow (missing secret binding) produces no startup error — the safe-outputs server starts, then fails later when it tries to authenticate with the literal string
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Smoke test reply: threading check passed here. Warning Firewall blocked 6 domainsThe following domains were blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "accounts.google.com"
- "android.clients.google.com"
- "clients2.google.com"
- "contentautofill.googleapis.com"
- "safebrowsingohttpgateway.googleapis.com"
- "www.google.com"See Network Configuration for more information.
|
||
| * @typedef {Object} LoadConfigResult | ||
| * @property {Record<string, any>} config - The processed configuration | ||
|
|
@@ -30,7 +62,7 @@ function loadConfig(server) { | |
| server.debug(`Config file content length: ${configFileContent.length} characters`); | ||
| // Don't log raw content to avoid exposing sensitive configuration data | ||
| server.debug(`Config file read successfully, attempting to parse JSON`); | ||
| safeOutputsConfigRaw = JSON.parse(configFileContent); | ||
| safeOutputsConfigRaw = resolveEnvPlaceholders(JSON.parse(configFileContent)); | ||
| server.debug(`Successfully parsed config from file with ${Object.keys(safeOutputsConfigRaw).length} configuration keys`); | ||
| } else { | ||
| server.debug(`Config file does not exist at: ${configPath}`); | ||
|
|
@@ -44,7 +76,7 @@ function loadConfig(server) { | |
| } | ||
|
|
||
| const safeOutputsConfig = Object.fromEntries(Object.entries(safeOutputsConfigRaw).map(([k, v]) => [k.replace(/-/g, "_"), v])); | ||
| server.debug(`Final processed config: ${JSON.stringify(safeOutputsConfig)}`); | ||
| server.debug(`Final processed config: ${JSON.stringify(redactSensitiveConfig(safeOutputsConfig))}`); | ||
|
|
||
| // Handle GH_AW_SAFE_OUTPUTS with default fallback | ||
| // Default is /opt (read-only mount for agent container) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
redactSensitiveConfigis duplicated verbatim across two modules. The two copies will diverge silently when the sensitive-key pattern is updated — one file gets patched, the other doesn't.💡 Suggested fix
Extract into a shared
config_redaction.cjsand require it from both files:This is already a two-file inconsistency:
safe_outputs_config.cjsextractsisSensitiveConfigKeyas a named helper whilesafe_output_handler_manager.cjsinlines the regex. Centralising removes the surface area for divergence.