diff --git a/README.md b/README.md index bc28e13..16fcc20 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ jobs: | `err-ignore` | `''` | Path to a file containing regex patterns for error-level changes to ignore | file path | | `warn-ignore` | `''` | Path to a file containing regex patterns for warning-level changes to ignore | file path | | `output-to-file` | `''` | Write output to this file path instead of stdout | file path | +| `allow-external-refs` | `false` | Resolve external `$ref`s. Defaults to `false` to prevent SSRF on untrusted pull requests. Set `true` if your spec references external URLs or loads split files by file path | `true`, `false` | ### Generate a changelog @@ -126,6 +127,7 @@ jobs: | `case-insensitive-headers` | `false` | Compare headers case-insensitively | `true`, `false` | | `template` | `''` | Custom Go template for output formatting | Go template string | | `output-to-file` | `''` | Write output to this file path instead of stdout | file path | +| `allow-external-refs` | `false` | Resolve external `$ref`s. Defaults to `false` to prevent SSRF on untrusted pull requests. Set `true` if your spec references external URLs or loads split files by file path | `true`, `false` | ### Generate a diff report @@ -160,6 +162,7 @@ jobs: | `composed` | `false` | Run in composed mode | `true`, `false` | | `flatten-allof` | `false` | Merge allOf subschemas into a single schema before diff | `true`, `false` | | `output-to-file` | `''` | Write output to this file path instead of stdout | file path | +| `allow-external-refs` | `false` | Resolve external `$ref`s. Defaults to `false` to prevent SSRF on untrusted pull requests. Set `true` if your spec references external URLs or loads split files by file path | `true`, `false` | ### Validate a single spec @@ -184,7 +187,7 @@ jobs: |---|---|---|---| | `spec` | — (required) | Path to the OpenAPI spec to validate | file path, URL, git ref | | `fail-on` | `''` | Fail with exit code 1 when a finding is at or above this severity (empty uses the oasdiff default, `ERR`) | `ERR`, `WARN`, `INFO` | -| `allow-external-refs` | `true` | Resolve external `$ref`s; set `false` to prevent SSRF when validating untrusted specs | `true`, `false` | +| `allow-external-refs` | `false` | Resolve external `$ref`s. Defaults to `false` to prevent SSRF on untrusted pull requests. Set `true` if your spec references external URLs | `true`, `false` | For a non-blocking, report-only run, leave `fail-on` and set `continue-on-error: true` on the step. Outputs: `findings` (total), `error_count`, `warning_count`, `info_count`. @@ -292,5 +295,6 @@ Each **Review** link opens a hosted page with a side-by-side spec diff and **App | `include-path-params` | `false` | Include path parameter names in endpoint matching | `true`, `false` | | `exclude-elements` | `''` | Exclude certain kinds of changes from the output | `endpoints`, `request`, `response` (comma-separated) | | `composed` | `false` | Run in composed mode | `true`, `false` | +| `allow-external-refs` | `false` | Resolve external `$ref`s. Defaults to `false` to prevent SSRF on untrusted pull requests. Set `true` if your spec references external URLs or loads split files by file path | `true`, `false` | [Get oasdiff Pro →](https://www.oasdiff.com/pricing) diff --git a/breaking/action.yml b/breaking/action.yml index ef7c00f..14229fb 100644 --- a/breaking/action.yml +++ b/breaking/action.yml @@ -52,6 +52,10 @@ inputs: description: 'Output to a file at the given path' required: false default: '' + allow-external-refs: + description: 'Allow external $refs in the spec; disable to prevent SSRF when processing untrusted specs. Defaults to false (safe for CI on untrusted pull requests).' + required: false + default: 'false' outputs: breaking: description: 'Output summary of API breaking changes, encompassing both warnings and errors' @@ -75,3 +79,4 @@ runs: - ${{ inputs.err-ignore }} - ${{ inputs.warn-ignore }} - ${{ inputs.output-to-file }} + - ${{ inputs.allow-external-refs }} diff --git a/breaking/entrypoint.sh b/breaking/entrypoint.sh index 8033425..a1c4bc8 100755 --- a/breaking/entrypoint.sh +++ b/breaking/entrypoint.sh @@ -20,6 +20,7 @@ readonly flatten_allof="${11}" readonly err_ignore="${12}" readonly warn_ignore="${13}" readonly output_to_file="${14}" +readonly allow_external_refs="${15}" write_output () { local output="$1" @@ -44,6 +45,11 @@ echo "running oasdiff breaking... base: $base, revision: $revision, fail_on: $fa # Build flags to pass in command flags="" +# allow-external-refs defaults to false (safe for CI on untrusted PRs); pass +# whatever the input resolved to so the explicit action input is authoritative. +if [ -n "$allow_external_refs" ]; then + flags="$flags --allow-external-refs=$allow_external_refs" +fi if [ "$include_path_params" = "true" ]; then flags="$flags --include-path-params" fi @@ -86,7 +92,15 @@ if [ -n "$fail_on" ]; then fail_on_flag="--fail-on $fail_on" fi exit_code=0 -breaking_changes=$(oasdiff breaking "$base" "$revision" $flags $fail_on_flag) || exit_code=$? +_err=$(mktemp) +breaking_changes=$(oasdiff breaking "$base" "$revision" $flags $fail_on_flag 2>"$_err") || exit_code=$? +[ -s "$_err" ] && cat "$_err" >&2 +# Exit code 123 = oasdiff refused a disallowed external $ref (stable contract, +# not message text). Surface the action-specific remedy. +if [ "$exit_code" -eq 123 ]; then + echo "::error::oasdiff: this spec resolves external \$refs, which are disabled by default to prevent SSRF on untrusted pull requests. If the spec is trusted, set 'allow-external-refs: true' on the oasdiff action step." +fi +rm -f "$_err" # Run 2: render annotations to stdout via --format githubactions so # GitHub parses them onto the PR's "Files changed" tab. Tolerate diff --git a/changelog/action.yml b/changelog/action.yml index 3f14cd9..00a5a7b 100644 --- a/changelog/action.yml +++ b/changelog/action.yml @@ -55,6 +55,10 @@ inputs: description: 'Output level: INFO (default), WARN, or ERR' required: false default: '' + allow-external-refs: + description: 'Allow external $refs in the spec; disable to prevent SSRF when processing untrusted specs. Defaults to false (safe for CI on untrusted pull requests).' + required: false + default: 'false' outputs: changelog: description: 'Output summary of API changelog' @@ -78,4 +82,5 @@ runs: - ${{ inputs.format }} - ${{ inputs.template }} - ${{ inputs.level }} + - ${{ inputs.allow-external-refs }} diff --git a/changelog/entrypoint.sh b/changelog/entrypoint.sh index 1aebe8a..e4318f2 100755 --- a/changelog/entrypoint.sh +++ b/changelog/entrypoint.sh @@ -39,11 +39,17 @@ readonly case_insensitive_headers="${11}" readonly format="${12}" readonly template="${13}" readonly level="${14}" +readonly allow_external_refs="${15}" echo "running oasdiff changelog base: $base, revision: $revision, include_path_params: $include_path_params, exclude_elements: $exclude_elements, filter_extension: $filter_extension, composed: $composed, flatten_allof: $flatten_allof, output_to_file: $output_to_file, prefix_base: $prefix_base, prefix_revision: $prefix_revision, case_insensitive_headers: $case_insensitive_headers, format: $format, template: $template, level: $level" # Build flags to pass in command flags="" +# allow-external-refs defaults to false (safe for CI on untrusted PRs); pass +# whatever the input resolved to so the explicit action input is authoritative. +if [ -n "$allow_external_refs" ]; then + flags="$flags --allow-external-refs=$allow_external_refs" +fi if [ "$include_path_params" = "true" ]; then flags="$flags --include-path-params" fi @@ -89,11 +95,24 @@ echo "flags: $flags" delimiter=$(cat /proc/sys/kernel/random/uuid | tr -d '-') echo "changelog<<$delimiter" >>"$GITHUB_OUTPUT" +exit_code=0 +_err=$(mktemp) if [ -n "$flags" ]; then - output=$(oasdiff changelog "$base" "$revision" $flags) + output=$(oasdiff changelog "$base" "$revision" $flags 2>"$_err") || exit_code=$? else - output=$(oasdiff changelog "$base" "$revision") + output=$(oasdiff changelog "$base" "$revision" 2>"$_err") || exit_code=$? +fi +if [ "$exit_code" -ne 0 ]; then + [ -s "$_err" ] && cat "$_err" >&2 + # Exit code 123 = oasdiff refused a disallowed external $ref (stable + # contract, not message text). Surface the action-specific remedy. + if [ "$exit_code" -eq 123 ]; then + echo "::error::oasdiff: this spec resolves external \$refs, which are disabled by default to prevent SSRF on untrusted pull requests. If the spec is trusted, set 'allow-external-refs: true' on the oasdiff action step." + fi + rm -f "$_err" + exit "$exit_code" fi +rm -f "$_err" if [ -n "$output" ] && ! echo "$output" | head -n 1 | grep -q "^No "; then write_output "$output" diff --git a/diff/action.yml b/diff/action.yml index 847b152..750d71f 100644 --- a/diff/action.yml +++ b/diff/action.yml @@ -39,6 +39,10 @@ inputs: description: 'Output to a file at the given path' required: false default: '' + allow-external-refs: + description: 'Allow external $refs in the spec; disable to prevent SSRF when processing untrusted specs. Defaults to false (safe for CI on untrusted pull requests).' + required: false + default: 'false' outputs: diff: description: 'Output summary of API diff' @@ -56,3 +60,4 @@ runs: - ${{ inputs.composed }} - ${{ inputs.flatten-allof }} - ${{ inputs.output-to-file }} + - ${{ inputs.allow-external-refs }} diff --git a/diff/entrypoint.sh b/diff/entrypoint.sh index 186560b..3e32d15 100755 --- a/diff/entrypoint.sh +++ b/diff/entrypoint.sh @@ -35,11 +35,17 @@ readonly filter_extension="$7" readonly composed="$8" readonly flatten_allof="$9" readonly output_to_file="${10}" +readonly allow_external_refs="${11}" echo "running oasdiff diff base: $base, revision: $revision, format: $format, fail_on_diff: $fail_on_diff, include_path_params: $include_path_params, exclude_elements: $exclude_elements, filter_extension: $filter_extension, composed: $composed, flatten_allof: $flatten_allof, output_to_file: $output_to_file" # Build flags to pass in command flags="" +# allow-external-refs defaults to false (safe for CI on untrusted PRs); pass +# whatever the input resolved to so the explicit action input is authoritative. +if [ -n "$allow_external_refs" ]; then + flags="$flags --allow-external-refs=$allow_external_refs" +fi if [ "$format" != "yaml" ]; then flags="$flags --format $format" fi @@ -75,11 +81,19 @@ echo "diff<<$delimiter" >>"$GITHUB_OUTPUT" # Capture the exit code from oasdiff command while still getting the output exit_code=0 +_err=$(mktemp) if [ -n "$flags" ]; then - output=$(oasdiff diff "$base" "$revision" $flags) || exit_code=$? + output=$(oasdiff diff "$base" "$revision" $flags 2>"$_err") || exit_code=$? else - output=$(oasdiff diff "$base" "$revision") || exit_code=$? + output=$(oasdiff diff "$base" "$revision" 2>"$_err") || exit_code=$? +fi +[ -s "$_err" ] && cat "$_err" >&2 +# Exit code 123 = oasdiff refused a disallowed external $ref (stable contract, +# not message text). Surface the action-specific remedy. +if [ "$exit_code" -eq 123 ]; then + echo "::error::oasdiff: this spec resolves external \$refs, which are disabled by default to prevent SSRF on untrusted pull requests. If the spec is trusted, set 'allow-external-refs: true' on the oasdiff action step." fi +rm -f "$_err" if [ -n "$output" ]; then write_output "$output" diff --git a/pr-comment/action.yml b/pr-comment/action.yml index e046b59..6833384 100644 --- a/pr-comment/action.yml +++ b/pr-comment/action.yml @@ -30,6 +30,10 @@ inputs: description: 'oasdiff service base URL (override for testing)' required: false default: 'https://api.oasdiff.com' + allow-external-refs: + description: 'Allow external $refs in the spec; disable to prevent SSRF when processing untrusted specs. Defaults to false (safe for CI on untrusted pull requests).' + required: false + default: 'false' runs: using: 'docker' image: 'Dockerfile' @@ -42,3 +46,4 @@ runs: - ${{ inputs.oasdiff-token }} - ${{ inputs.github-token }} - ${{ inputs.service-url }} + - ${{ inputs.allow-external-refs }} diff --git a/pr-comment/entrypoint.sh b/pr-comment/entrypoint.sh index 8ab4340..cafd5c0 100755 --- a/pr-comment/entrypoint.sh +++ b/pr-comment/entrypoint.sh @@ -14,11 +14,17 @@ readonly composed="$5" readonly oasdiff_token="$6" readonly github_token="$7" readonly service_url="${8:-https://api.oasdiff.com}" +readonly allow_external_refs="${9}" echo "running oasdiff pr-comment base: $base, revision: $revision, include_path_params: $include_path_params, exclude_elements: $exclude_elements, composed: $composed" # Build flags flags="" +# allow-external-refs defaults to false (safe for CI on untrusted PRs); pass +# whatever the input resolved to so the explicit action input is authoritative. +if [ -n "$allow_external_refs" ]; then + flags="$flags --allow-external-refs=$allow_external_refs" +fi if [ "$include_path_params" = "true" ]; then flags="$flags --include-path-params" fi @@ -36,11 +42,20 @@ fi # us from collecting the JSON. Real failures (missing file, parse error) # still abort because they leave $changelog empty. oasdiff_exit=0 -changelog=$(oasdiff changelog "$base" "$revision" --format json $flags) || oasdiff_exit=$? +_err=$(mktemp) +changelog=$(oasdiff changelog "$base" "$revision" --format json $flags 2>"$_err") || oasdiff_exit=$? if [ "$oasdiff_exit" -ne 0 ] && [ -z "$changelog" ]; then + [ -s "$_err" ] && cat "$_err" >&2 + # Exit code 123 = oasdiff refused a disallowed external $ref (stable + # contract, not message text). Surface the action-specific remedy. + if [ "$oasdiff_exit" -eq 123 ]; then + echo "::error::oasdiff: this spec resolves external \$refs, which are disabled by default to prevent SSRF on untrusted pull requests. If the spec is trusted, set 'allow-external-refs: true' on the oasdiff action step." + fi + rm -f "$_err" echo "ERROR: oasdiff exited $oasdiff_exit with no output" >&2 exit $oasdiff_exit fi +rm -f "$_err" # If no changes, use empty array if [ -z "$changelog" ] || [ "$changelog" = "null" ] || [ "$changelog" = "[]" ]; then diff --git a/validate/action.yml b/validate/action.yml index b358264..60ec87d 100644 --- a/validate/action.yml +++ b/validate/action.yml @@ -9,9 +9,9 @@ inputs: required: false default: '' allow-external-refs: - description: 'Allow external $refs in the spec; disable to prevent SSRF when validating untrusted specs' + description: 'Allow external $refs in the spec; disable to prevent SSRF when validating untrusted specs. Defaults to false (safe for CI on untrusted pull requests).' required: false - default: 'true' + default: 'false' outputs: findings: description: 'Total number of findings reported by validate (0 if the spec is valid)' diff --git a/validate/entrypoint.sh b/validate/entrypoint.sh index 4899134..6db87f1 100755 --- a/validate/entrypoint.sh +++ b/validate/entrypoint.sh @@ -12,13 +12,14 @@ readonly allow_external_refs="$3" echo "running oasdiff validate... spec: $spec, fail_on: $fail_on, allow_external_refs: $allow_external_refs" -# Build flags. --allow-external-refs defaults to true in oasdiff, so only -# pass it when the input opts out. --fail-on defaults to ERR in oasdiff +# Build flags. The action input allow-external-refs defaults to false (safe for +# CI on untrusted PRs); pass whatever it resolved to so the explicit input is +# authoritative over any oasdiff.yaml value. --fail-on defaults to ERR in oasdiff # (errors fail the build; warnings and info are reported but don't), so only # pass it when the input overrides the threshold. flags="" -if [ "$allow_external_refs" = "false" ]; then - flags="$flags --allow-external-refs=false" +if [ -n "$allow_external_refs" ]; then + flags="$flags --allow-external-refs=$allow_external_refs" fi if [ -n "$fail_on" ]; then flags="$flags --fail-on $fail_on" @@ -31,7 +32,15 @@ echo "flags: $flags" # threshold, 0 otherwise). Tolerate non-zero so we can still set the outputs # below; the exit code is reapplied at the end. exit_code=0 -oasdiff validate $flags --format githubactions "$spec" || exit_code=$? +_err=$(mktemp) +oasdiff validate $flags --format githubactions "$spec" 2>"$_err" || exit_code=$? +[ -s "$_err" ] && cat "$_err" >&2 +# Exit code 123 = oasdiff refused a disallowed external $ref (stable contract, +# not message text). Surface the action-specific remedy. +if [ "$exit_code" -eq 123 ]; then + echo "::error::oasdiff: this spec resolves external \$refs, which are disabled by default to prevent SSRF on untrusted pull requests. If the spec is trusted, set 'allow-external-refs: true' on the oasdiff validate step." +fi +rm -f "$_err" # Run 2: text format, captured for the finding count. Tolerate non-zero # exit (the authoritative decision is already captured above).