From 6bd534d49f00754fd3e7ba0ad34ed5f2bc9bc535 Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Mon, 7 Oct 2024 21:30:01 +0100 Subject: [PATCH 1/4] Adds new/missing CSP parmeters in security headers --- docs/Tutorials/Middleware/Types/Security.md | 69 ++++++----- src/Private/Security.ps1 | 38 +++++- src/Public/Security.ps1 | 129 +++++++++++++++++++- 3 files changed, 197 insertions(+), 39 deletions(-) diff --git a/docs/Tutorials/Middleware/Types/Security.md b/docs/Tutorials/Middleware/Types/Security.md index 69f3d60f7..0adee8603 100644 --- a/docs/Tutorials/Middleware/Types/Security.md +++ b/docs/Tutorials/Middleware/Types/Security.md @@ -2,7 +2,7 @@ The security headers middleware runs at the beginning of every request, and if any security headers are defined they will be added onto the response. -The following headers are currently supported, but you can add custom header values: +The following headers are currently supported, but you can add custom header values via [`Add-PodeSecurityHeader`](../../../../Functions/Security/Add-PodeSecurityHeader) for any missing: * Access-Control-Max-Age * Access-Control-Allow-Methods @@ -13,6 +13,7 @@ The following headers are currently supported, but you can add custom header val * Cross-Origin-Opener-Policy * Strict-Transport-Security * Content-Security-Policy +* Content-Security-Policy-Report-Only * X-XSS-Protection * Permissions-Policy * X-Frame-Options @@ -37,21 +38,21 @@ To remove all configured values, use [`Remove-PodeSecurity`](../../../../Functio The following values are used for each header when the `Simple` type is supplied: -| Name | Value | -| ---- | ----- | -| Access-Control-Max-Age | 7200 | -| Access-Control-Allow-Origin | * | -| Access-Control-Allow-Methods | * | -| Access-Control-Allow-Headers | * | -| Cross-Origin-Embedder-Policy | require-corp | -| Cross-Origin-Resource-Policy | same-origin | -| Cross-Origin-Opener-Policy | same-origin | -| Content-Security-Policy | default-src 'self' | -| X-XSS-Protection | 0 | -| Permissions-Policy | accelerometer=(), autoplay=(self), camera=(), display-capture=(self), fullscreen=(self), geolocation=(self), gyroscope=(self), magnetometer=(self), microphone=(), payment=(), picture-in-picture=(self), sync-xhr=(), usb=() | -| X-Frame-Options | SAMEORIGIN | -| X-Content-Type-Options | nosniff | -| Referred-Policy | strict-origin | +| Name | Value | +| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Access-Control-Max-Age | 7200 | +| Access-Control-Allow-Origin | * | +| Access-Control-Allow-Methods | * | +| Access-Control-Allow-Headers | * | +| Cross-Origin-Embedder-Policy | require-corp | +| Cross-Origin-Resource-Policy | same-origin | +| Cross-Origin-Opener-Policy | same-origin | +| Content-Security-Policy | default-src 'self' | +| X-XSS-Protection | 0 | +| Permissions-Policy | accelerometer=(), autoplay=(self), camera=(), display-capture=(self), fullscreen=(self), geolocation=(self), gyroscope=(self), magnetometer=(self), microphone=(), payment=(), picture-in-picture=(self), sync-xhr=(), usb=() | +| X-Frame-Options | SAMEORIGIN | +| X-Content-Type-Options | nosniff | +| Referred-Policy | strict-origin | The Server header is also hidden. @@ -59,22 +60,22 @@ The Server header is also hidden. The following values are used for each header when the `Strict` type is supplied: -| Name | Value | -| ---- | ----- | -| Access-Control-Max-Age | 7200 | -| Access-Control-Allow-Methods | * | -| Access-Control-Allow-Origin | * | -| Access-Control-Allow-Headers | * | -| Cross-Origin-Embedder-Policy | require-corp | -| Cross-Origin-Resource-Policy | same-origin | -| Cross-Origin-Opener-Policy | same-origin | -| Strict-Transport-Security | max-age=31536000; includeSubDomains | -| Content-Security-Policy | default-src 'self' | -| X-XSS-Protection | 0 | -| Permissions-Policy | accelerometer=(), autoplay=(self), camera=(), display-capture=(self), fullscreen=(self), geolocation=(self), gyroscope=(self), magnetometer=(self), microphone=(), payment=(), picture-in-picture=(self), sync-xhr=(), usb=() | -| X-Frame-Options | DENY | -| X-Content-Type-Options | nosniff | -| Referred-Policy | no-referrer | +| Name | Value | +| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Access-Control-Max-Age | 7200 | +| Access-Control-Allow-Methods | * | +| Access-Control-Allow-Origin | * | +| Access-Control-Allow-Headers | * | +| Cross-Origin-Embedder-Policy | require-corp | +| Cross-Origin-Resource-Policy | same-origin | +| Cross-Origin-Opener-Policy | same-origin | +| Strict-Transport-Security | max-age=31536000; includeSubDomains | +| Content-Security-Policy | default-src 'self' | +| X-XSS-Protection | 0 | +| Permissions-Policy | accelerometer=(), autoplay=(self), camera=(), display-capture=(self), fullscreen=(self), geolocation=(self), gyroscope=(self), magnetometer=(self), microphone=(), payment=(), picture-in-picture=(self), sync-xhr=(), usb=() | +| X-Frame-Options | DENY | +| X-Content-Type-Options | nosniff | +| Referred-Policy | no-referrer | The Server header is also hidden. @@ -153,12 +154,14 @@ The following functions exist: * [`Set-PodeSecurityContentSecurityPolicy`](../../../../Functions/Security/Set-PodeSecurityContentSecurityPolicy) * [`Remove-PodeSecurityContentSecurityPolicy`](../../../../Functions/Security/Remove-PodeSecurityContentSecurityPolicy) -The `Content-Security-Policy` header controls a whitelist of approved sourced from which the browser can load resoures. For example: +The `Content-Security-Policy` header controls a whitelist of approved sources from which the browser can load resources. For example: ```powershell Set-PodeSecurityContentSecurityPolicy -Default 'self' -Image 'self', 'data' ``` +By supplying the `-ReportOnly` switch, the `Content-Security-Policy-Report-Only` header will be used instead. + ### Permissions Policy The following functions exist: diff --git a/src/Private/Security.ps1 b/src/Private/Security.ps1 index c5f475db4..eabbaa38c 100644 --- a/src/Private/Security.ps1 +++ b/src/Private/Security.ps1 @@ -1037,18 +1037,32 @@ function Protect-PodeContentSecurityKeyword { $Name = $Name.ToLowerInvariant() $keywords = @( + # standard keywords 'none', 'self', + 'strict-dynamic', + 'report-sample', + 'inline-speculation-rules', + + # unsafe keywords 'unsafe-inline', - 'unsafe-eval' + 'unsafe-eval', + 'unsafe-hashes', + 'wasm-unsafe-eval' ) $schemes = @( 'http', 'https', + 'data', + 'blob', + 'filesystem', + 'mediastream', 'ws', 'wss', - 'data', + 'ftp', + 'mailto', + 'tel', 'file' ) @@ -1183,8 +1197,20 @@ function Set-PodeSecurityContentSecurityPolicyInternal { Protect-PodeContentSecurityKeyword -Name 'base-uri' -Value $Params.BaseUri -Append:$Append Protect-PodeContentSecurityKeyword -Name 'form-action' -Value $Params.FormAction -Append:$Append Protect-PodeContentSecurityKeyword -Name 'frame-ancestors' -Value $Params.FrameAncestor -Append:$Append + Protect-PodeContentSecurityKeyword -Name 'fenched-frame-src' -Value $Params.FencedFrame -Append:$Append + Protect-PodeContentSecurityKeyword -Name 'prefetch-src' -Value $Params.Prefetch -Append:$Append + Protect-PodeContentSecurityKeyword -Name 'script-src-attr' -Value $Params.ScriptAttr -Append:$Append + Protect-PodeContentSecurityKeyword -Name 'script-src-elem' -Value $Params.ScriptElem -Append:$Append + Protect-PodeContentSecurityKeyword -Name 'style-src-attr' -Value $Params.StyleAttr -Append:$Append + Protect-PodeContentSecurityKeyword -Name 'style-src-elem' -Value $Params.StyleElem -Append:$Append + Protect-PodeContentSecurityKeyword -Name 'worker-src' -Value $Params.Worker -Append:$Append ) + # add "report-uri" if supplied + if (![string]::IsNullOrWhiteSpace($Params.ReportUri)) { + $values += "report-uri $($Params.ReportUri)".Trim() + } + if (![string]::IsNullOrWhiteSpace($Params.Sandbox) -and ($Params.Sandbox -ine 'None')) { $values += "sandbox $($Params.Sandbox.ToLowerInvariant())".Trim() } @@ -1202,7 +1228,13 @@ function Set-PodeSecurityContentSecurityPolicyInternal { # Add the Content Security Policy header to the response or relevant context. This cmdlet # sets the HTTP header with the name 'Content-Security-Policy' and the constructed value. - Add-PodeSecurityHeader -Name 'Content-Security-Policy' -Value $value + # if ReportOnly is set, the header name is set to 'Content-Security-Policy-Report-Only'. + $header = 'Content-Security-Policy' + if ($Params.ReportOnly) { + $header = 'Content-Security-Policy-Report-Only' + } + + Add-PodeSecurityHeader -Name $header -Value $value # this is done to explicitly disable XSS auditors in modern browsers # as having it enabled has now been found to cause more vulnerabilities diff --git a/src/Public/Security.ps1 b/src/Public/Security.ps1 index ee5ab7947..9a76b9e4a 100644 --- a/src/Public/Security.ps1 +++ b/src/Public/Security.ps1 @@ -302,15 +302,42 @@ The values to use for the FormAction portion of the header. .PARAMETER FrameAncestor The values to use for the FrameAncestor portion of the header. +.PARAMETER FencedFrame +The values to use for the FencedFrame portion of the header. + +.PARAMETER Prefetch +The values to use for the Prefetch portion of the header. + +.PARAMETER ScriptAttr +The values to use for the ScriptAttr portion of the header. + +.PARAMETER ScriptElem +The values to use for the ScriptElem portion of the header. + +.PARAMETER StyleAttr +The values to use for the StyleAttr portion of the header. + +.PARAMETER StyleElem +The values to use for the StyleElem portion of the header. + +.PARAMETER Worker +The values to use for the Worker portion of the header. + .PARAMETER Sandbox The value to use for the Sandbox portion of the header. +.PARAMETER ReportUri +The value to use for the ReportUri portion of the header. + .PARAMETER UpgradeInsecureRequests If supplied, the header will have the upgrade-insecure-requests value added. .PARAMETER XssBlock If supplied, the X-XSS-Protection header will be set to blocking mode. (Default: Off) +.PARAMETER ReportOnly +If supplied, the header will be set as a report-only header. + .EXAMPLE Set-PodeSecurityContentSecurityPolicy -Default 'self' #> @@ -373,6 +400,34 @@ function Set-PodeSecurityContentSecurityPolicy { [string[]] $FrameAncestor, + [Parameter()] + [string[]] + $FencedFrame, + + [Parameter()] + [string[]] + $Prefetch, + + [Parameter()] + [string[]] + $ScriptAttr, + + [Parameter()] + [string[]] + $ScriptElem, + + [Parameter()] + [string[]] + $StyleAttr, + + [Parameter()] + [string[]] + $StyleElem, + + [Parameter()] + [string[]] + $Worker, + [Parameter()] [ValidateSet('', 'Allow-Downloads', 'Allow-Downloads-Without-User-Activation', 'Allow-Forms', 'Allow-Modals', 'Allow-Orientation-Lock', 'Allow-Pointer-Lock', 'Allow-Popups', 'Allow-Popups-To-Escape-Sandbox', 'Allow-Presentation', 'Allow-Same-Origin', 'Allow-Scripts', @@ -380,11 +435,18 @@ function Set-PodeSecurityContentSecurityPolicy { [string] $Sandbox = 'None', + [Parameter()] + [string] + $ReportUri, + [switch] $UpgradeInsecureRequests, [switch] - $XssBlock + $XssBlock, + + [switch] + $ReportOnly ) Set-PodeSecurityContentSecurityPolicyInternal -Params $PSBoundParameters @@ -439,17 +501,43 @@ The values to add for the FormAction portion of the header. .PARAMETER FrameAncestor The values to add for the FrameAncestor portion of the header. +.PARAMETER FencedFrame +The values to add for the FencedFrame portion of the header. + +.PARAMETER Prefetch +The values to add for the Prefetch portion of the header. + +.PARAMETER ScriptAttr +The values to add for the ScriptAttr portion of the header. + +.PARAMETER ScriptElem +The values to add for the ScriptElem portion of the header. + +.PARAMETER StyleAttr +The values to add for the StyleAttr portion of the header. + +.PARAMETER StyleElem +The values to add for the StyleElem portion of the header. + +.PARAMETER Worker +The values to add for the Worker portion of the header. + .PARAMETER Sandbox The value to use for the Sandbox portion of the header. +.PARAMETER ReportUri +The value to use for the ReportUri portion of the header. + .PARAMETER UpgradeInsecureRequests If supplied, the header will have the upgrade-insecure-requests value added. +.PARAMETER ReportOnly +If supplied, the header will be set as a report-only header. + .EXAMPLE Add-PodeSecurityContentSecurityPolicy -Default '*.twitter.com' -Image 'data' #> function Add-PodeSecurityContentSecurityPolicy { - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSPossibleIncorrectComparisonWithNull', '')] [CmdletBinding()] param( [Parameter()] @@ -508,6 +596,34 @@ function Add-PodeSecurityContentSecurityPolicy { [string[]] $FrameAncestor, + [Parameter()] + [string[]] + $FencedFrame, + + [Parameter()] + [string[]] + $Prefetch, + + [Parameter()] + [string[]] + $ScriptAttr, + + [Parameter()] + [string[]] + $ScriptElem, + + [Parameter()] + [string[]] + $StyleAttr, + + [Parameter()] + [string[]] + $StyleElem, + + [Parameter()] + [string[]] + $Worker, + [Parameter()] [ValidateSet('', 'Allow-Downloads', 'Allow-Downloads-Without-User-Activation', 'Allow-Forms', 'Allow-Modals', 'Allow-Orientation-Lock', 'Allow-Pointer-Lock', 'Allow-Popups', 'Allow-Popups-To-Escape-Sandbox', 'Allow-Presentation', 'Allow-Same-Origin', 'Allow-Scripts', @@ -515,8 +631,15 @@ function Add-PodeSecurityContentSecurityPolicy { [string] $Sandbox = 'None', + [Parameter()] + [string] + $ReportUri, + + [switch] + $UpgradeInsecureRequests, + [switch] - $UpgradeInsecureRequests + $ReportOnly ) Set-PodeSecurityContentSecurityPolicyInternal -Params $PSBoundParameters -Append From e828c0dba703509f66d8689bcaa34c5342f40f72 Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Fri, 11 Oct 2024 19:29:46 +0100 Subject: [PATCH 2/4] fix for pwsh installer location changing --- pode.build.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pode.build.ps1 b/pode.build.ps1 index 2d2be077d..fb20fc47f 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -800,12 +800,12 @@ Task SetupPowerShell { })[$os] # build the blob name - $blobName = "v$($PowerShellVersion -replace '\.', '-')" + $blobName = "v$($PowerShellVersion)" # download the package to a temp location $outputFile = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath $packageName $downloadParams = @{ - Uri = "https://pscoretestdata.blob.core.windows.net/$($blobName)/$($packageName)" + Uri = "https://powershellinfraartifacts-gkhedzdeaghdezhr.z01.azurefd.net/install/$($blobName)/$($packageName)" OutFile = $outputFile ErrorAction = 'Stop' } From ed958c923187472b4ce902586ccbad796ab7b84e Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Fri, 11 Oct 2024 19:39:45 +0100 Subject: [PATCH 3/4] call new pwsh install url first, then old --- pode.build.ps1 | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/pode.build.ps1 b/pode.build.ps1 index fb20fc47f..34a72f0ce 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -799,29 +799,41 @@ Task SetupPowerShell { osx = "powershell-$($PowerShellVersion)-$($os)-$($arch).tar.gz" })[$os] - # build the blob name - $blobName = "v$($PowerShellVersion)" + # build the URL + $urls = @{ + Old = "https://pscoretestdata.blob.core.windows.net/v$($PowerShellVersion -replace '\.', '-')/$($packageName)" + New = "https://powershellinfraartifacts-gkhedzdeaghdezhr.z01.azurefd.net/install/v$($PowerShellVersion)/$($packageName)" + } # download the package to a temp location $outputFile = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath $packageName $downloadParams = @{ - Uri = "https://powershellinfraartifacts-gkhedzdeaghdezhr.z01.azurefd.net/install/$($blobName)/$($packageName)" + Uri = $urls.New OutFile = $outputFile ErrorAction = 'Stop' } - Write-Host "Downloading $($packageName) from $($downloadParams.Uri)" Write-Host "Output file: $($outputFile)" - # retry the download 3 times, with a sleep of 10s between each attempt + # retry the download 6 times, with a sleep of 10s between each attempt, and altering between old and new URLs $counter = 0 $success = $false do { try { $counter++ - Write-Host "Attempt $($counter) of 3" + Write-Host "Attempt $($counter) of 6" + + # use new URL for odd attempts, and old URL for even attempts + if ($counter % 2 -eq 0) { + $downloadParams.Uri = $urls.Old + } + else { + $downloadParams.Uri = $urls.New + } + # download the package + Write-Host "Attempting download of $($packageName) from $($downloadParams.Uri)" Invoke-WebRequest @downloadParams $success = $true @@ -829,11 +841,11 @@ Task SetupPowerShell { } catch { $success = $false - if ($counter -ge 3) { - throw "Failed to download PowerShell package after 3 attempts. Error: $($_.Exception.Message)" + if ($counter -ge 6) { + throw "Failed to download PowerShell package after 6 attempts. Error: $($_.Exception.Message)" } - Start-Sleep -Seconds 10 + Start-Sleep -Seconds 5 } } while (!$success) From 9b2b8db3098f07685c671bee3fbf43f02adce9ee Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Fri, 11 Oct 2024 21:09:44 +0100 Subject: [PATCH 4/4] adds a CspReportOnly switch on Set-PodeSecurity --- src/Public/Security.ps1 | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Public/Security.ps1 b/src/Public/Security.ps1 index 9a76b9e4a..f53fcb2ca 100644 --- a/src/Public/Security.ps1 +++ b/src/Public/Security.ps1 @@ -14,6 +14,9 @@ If supplied, the Strict-Transport-Security header will be set. .PARAMETER XssBlock If supplied, the X-XSS-Protection header will be set to blocking mode. (Default: Off) +.PARAMETER CspReportOnly +If supplied, the Content-Security-Policy header will be set as the Content-Security-Policy-Report-Only header. + .EXAMPLE Set-PodeSecurity -Type Simple @@ -32,7 +35,10 @@ function Set-PodeSecurity { $UseHsts, [switch] - $XssBlock + $XssBlock, + + [switch] + $CspReportOnly ) # general headers @@ -55,7 +61,7 @@ function Set-PodeSecurity { Set-PodeSecurityCrossOrigin -Embed Require-Corp -Open Same-Origin -Resource Same-Origin Set-PodeSecurityAccessControl -Origin '*' -Methods '*' -Headers '*' -Duration 7200 - Set-PodeSecurityContentSecurityPolicy -Default 'self' -XssBlock:$XssBlock + Set-PodeSecurityContentSecurityPolicy -Default 'self' -XssBlock:$XssBlock -ReportOnly:$CspReportOnly # only add hsts if specifiec if ($UseHsts) {