How to not mask output in GitHub Actions?

Viewed 77

As part of a PowerShell Script in GitHub Actions, I am trying to output a json list of objects, to later re-use as a matrix of another job. With the following command I will be writing the output:

Write-Host "::set-output name=value::$( $array1 | ConvertTo-JSON -Compress)"

The output of variable $array1 (JSON-converted) should look like this:

[ 
  {
    "variable1": "hello1"
  },
  {
    "variable1": "hello2"
  }
]

Yet, the curly brackets are masked so the output looks like this:

[ 
  ***
    "variable1": "hello1"
  ***,
  ***
    "variable1": "hello2"
  ***
]

Also when simply echoing the JSON converted $array1, the above issue is there in GitHub Actions. Is there any way to overcome the masking of the curly brackets? When using PowerShell locally the list is outputted perfectly fine - only in GitHub Actions this issue persists.

$array1 is created like this:

$array1 = $()
foreach ($var in $vars) {
   # Do some stuff to get to output for each var
   $output = dostuff($var)
   $array1 += @(@{"variable1" = $output})

}
1 Answers

tl;dr - To stop the masking you'll need to stop referencing whatever Actions Secrets are equal to the values { and }.

Long Version

I can reproduce the behaviour with this GitHub Actions workflow file provided you first create two Actions Secrets that are in scope for your workflow (i.e. at Repository or Organization level):

Actions Secrets

Secret Name Value
LEFT_CURLY {
RIGHT_CURLY }

myWorkflow.yaml

The interesting bit is the activate-masking step...

name: myWorkflow

on:
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - id: define-output
        name: define output
        shell: pwsh
        run: |
          $array1 = @(
            [pscustomobject] @{
              "variable1" = "hello1"
            },
            [pscustomobject] @{
              "variable1" = "hello2"
            }
          );
          write-host "::set-output name=myjson::$( $array1 | ConvertTo-JSON -Compress)"

      - id: activate-masking
        name: activate masking
        shell: pwsh
        run: |
          # referencing a secret anywhere in a step activates
          # log masking for its value in the *entire* workflow
          write-host "left = '${{ secrets.LEFT_CURLY}}'"
          write-host "right = '${{ secrets.RIGHT_CURLY }}'"

      - id: consume-output
        name: consume output
        shell: pwsh
        run: |

          # decode and re-encode the data to remove "compressed"
          # output format as noted by @mklement0 in comments
          $json = '${{ steps.define-output.outputs.myjson }}'
          $data = $json | ConvertFrom-Json

          # write the json to the log output. if masking is activate
          # then any secrets in the output will be masked with "***"
          write-host ($data| ConvertTo-Json)

          # encode the json so it bypasses masking and we can get the true
          # value contained in the variable
          write-host "base64 = '$([System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($json)))'"

The output from the "consume output" step is as follows:

[v] consume output
 1  Run write-host "left = '***'"
 9  left = '***'
10  right = '***'
11  [
12    ***
13     "variable1": "hello1"
14    ***,
15    ***
16     "variable1": "hello2"
17    ***
18  ]
19  base64 = 'W3sidmFyaWFibGUxIjoiaGVsbG8xIn0seyJ2YXJpYWJsZTEiOiJoZWxsbzIifV0='

Note that the *** masking is obfuscation added to the log file only, not the variable value itself. If you decode the base64 string you'll end up with:

[{"variable1":"hello1"},{"variable1":"hello2"}]

so you can see the value of the variable is as expected.

Note also that masking only happens if a secret is referenced in a step - for example if you remove these lines from the activate-masking step it'll stop masking the values even though there's still Secrets defined in the workflow's scope:

# remove these lines to stop masking their values
# write-host "left = '${{ secrets.LEFT_CURLY}}'"
# write-host "right = '${{ secrets.RIGHT_CURLY }}'"

To stop the masking you'll need to stop referencing whatever Actions Secrets are equal to the values { and }.


Bonus Round

Note there's an injection vulnerability in my Action above for specially crafted json values due to malicious input - for example if I could somehow make the value of variable1 in your json end up being this cryptic string (e.g. by giving you it as user input):

}]';write-host aaa;$y='

then the Action above will execute this PowerShell:

$json = '[{"variable1":"}]';write-host aaa;$y='}]'

which is equivalent to

$json = '[{"variable1":"}]';
write-host aaa;
$y='}]'

and it will actually execute write-host aaa; as a command rather than as treating it as input data.

To avoid this, you might want to consider passing your outputs as base64-encoded strings - e.g.

- id: define-output
  ...
  $base64 = [System.Convert]::ToBase64String(
    [System.Text.Encoding]::UTF8.GetBytes(
      ($array1 | ConvertTo-JSON -Compress)
    )
  )
  write-host "::set-output name=myjson::$base64"

- id: consume-output
  ...
  $base64 = '${{ steps.define-output.outputs.myjson }}'
  $json = [System.Text.Encoding]::UTF8.GetString(
    [System.Convert]::FromBase64String(
      $base64
    )
  )

That way, your json data can't get inadvertently promoted to being executed as code...

Related