4

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})

}
  • What does `$array1` contain? – Mathias R. Jessen Aug 24 '22 at 15:52
  • @MathiasR.Jessen A list of variables, so it contains the expected output (when converted to JSON), so: ``` [ { "variable1": "hello1" }, { "variable1": "hello2" } ] ``` – Onno van der Horst Aug 24 '22 at 15:53
  • Okay, so it's already JSON? Then you can skip `ConvertTo-Json`... – Mathias R. Jessen Aug 24 '22 at 15:54
  • @MathiasR.Jessen nope it is a list of variables, so not in JSON yet.. – Onno van der Horst Aug 24 '22 at 15:56
  • "a list of variables" tells us next to nothing. Can you post sample code for recreating `$array1`? – Mathias R. Jessen Aug 24 '22 at 15:57
  • @MathiasR.Jessen Sorry, edited the post now with additional information on how the variable is created. – Onno van der Horst Aug 24 '22 at 16:04
  • 1
    Do you have any Actions “Secrets” defined in the repo / organisation? They’ll get masked in the *display* output. Admittedly, it would be a bit weird to have a Secret containing just ```{``` or ```}```, but that would explain the output… – mclayton Aug 24 '22 at 17:52
  • To test if it’s just a *display* issue, you could try base64 encoding the output and see what value that shows. You could then base64 decode that string on your machine to verify the original string, and if it looks correct then you know it’s got the right value even though the log output is masking parts of it. – mclayton Aug 24 '22 at 17:55

2 Answers2

3

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...

mclayton
  • 8,025
  • 2
  • 21
  • 26
  • Thanks a lot for all the research! I am not referencing any Repository / Environment /Org secrets, so I'm a bit confused. I will try the approach with Base64 variables. Yet one thing I should figure out is how to convert this base64 in a yaml input field. I would assume something like this: `example_matrix: needs: [gather_data] strategy: matrix: include: ${{ fromJson(fromBase64String(needs.gather_data.outputs.matrix)) }}` – Onno van der Horst Aug 25 '22 at 08:58
  • 1
    @OnnovanderHorst - it looks like secret masking is activated if the secret is referenced *anywhere* in the workflow, not just in the same step. I've updated my answer to reflect this. Maybe try creating a new minimal workflow with just your json code in it and see if that reproduces the issue... – mclayton Aug 25 '22 at 11:57
1

There is a great GitHub Actions feature - configuration variables.

This feature allows storing your non-sensitive data as plain text variables that can be reused across your workflows in your repository or organization.

Unlike the repository secrets, the values of configuration variables will not be masked during the workflow run.

For mo details, see this post: https://stackoverflow.com/a/75086864/7328018

Andrii Bodnar
  • 1,672
  • 2
  • 17
  • 24