-
-
Notifications
You must be signed in to change notification settings - Fork 0
feat(http-helmet): remove insecure response headers #148
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
base: dev
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@mcansh/http-helmet": patch | ||
| --- | ||
|
|
||
| Add `removeInsecureHeaders` to remove server-identifying response headers maintained by the OWASP Secure Headers Project. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| name: 🛡️ Update removal headers | ||
|
|
||
| on: | ||
| schedule: | ||
| - cron: "17 13 * * 1" | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: write | ||
| pull-requests: write | ||
|
|
||
| concurrency: | ||
| group: update-http-helmet-removal-headers | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| update: | ||
| name: 🛡️ Update removal headers | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 10 | ||
| steps: | ||
| - name: ⬇️ Checkout repo | ||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| fetch-depth: 0 | ||
| ref: ${{ github.event.repository.default_branch }} | ||
|
Comment on lines
+22
to
+26
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. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Disable credential persistence and configure git auth explicitly for the push.
🔒 Proposed fix - name: ⬇️ Checkout repo
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
+ persist-credentials: false
ref: ${{ github.event.repository.default_branch }} run: |
set -euo pipefail
+ gh auth setup-git
+
response_file=$(mktemp)As per the static analysis hints, this triggers 🧰 Tools🪛 zizmor (1.28.0)[warning] 22-26: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false (artipacked) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
|
|
||
| - name: 🛡️ Update removal headers and pull request | ||
| shell: bash | ||
| env: | ||
| BASE_BRANCH: ${{ github.event.repository.default_branch }} | ||
| CHANGESET_PATH: .changeset/update-http-helmet-removal-headers.md | ||
| GH_TOKEN: ${{ github.token }} | ||
| OUTPUT_PATH: packages/http-helmet/src/removal-headers.ts | ||
| SOURCE_URL: https://raw.githubusercontent.com/OWASP/www-project-secure-headers/refs/heads/master/ci/headers_remove.json | ||
| UPDATE_BRANCH: automation/update-removal-headers | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| response_file=$(mktemp) | ||
| trap 'rm -f "$response_file"' EXIT | ||
|
|
||
| curl \ | ||
| --fail \ | ||
| --location \ | ||
| --max-filesize 1048576 \ | ||
| --max-time 30 \ | ||
| --proto '=https' \ | ||
| --proto-redir '=https' \ | ||
| --retry 3 \ | ||
| --show-error \ | ||
| --silent \ | ||
| --tlsv1.2 \ | ||
| "$SOURCE_URL" \ | ||
| --output "$response_file" | ||
|
|
||
| response_size=$(wc -c < "$response_file") | ||
| if (( response_size > 1048576 )); then | ||
| echo "OWASP response exceeded the 1 MiB limit" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| jq --exit-status ' | ||
| .headers | ||
| | type == "array" | ||
| and length > 0 | ||
| and length <= 1000 | ||
| and (unique | length) == length | ||
| and all(.[]; | ||
| if type == "string" then | ||
| length > 0 | ||
| and length <= 256 | ||
| and test("\\A[!#$%&\u0027*+.^_`|~0-9A-Za-z-]+\\z") | ||
| else | ||
| false | ||
| end | ||
| ) | ||
| ' "$response_file" > /dev/null | ||
|
Comment on lines
+63
to
+78
|
||
|
|
||
| remote_branch_sha="" | ||
| if git ls-remote --exit-code --heads origin "refs/heads/$UPDATE_BRANCH" > /dev/null 2>&1; then | ||
| git fetch \ | ||
| --no-tags \ | ||
| origin \ | ||
| "+refs/heads/$UPDATE_BRANCH:refs/remotes/origin/$UPDATE_BRANCH" | ||
| remote_branch_sha=$(git rev-parse "refs/remotes/origin/$UPDATE_BRANCH") | ||
| fi | ||
|
|
||
| git switch \ | ||
| --force-create "$UPDATE_BRANCH" \ | ||
| "origin/$BASE_BRANCH" | ||
|
|
||
| { | ||
| printf 'export const removalHeaders = [\n' | ||
| jq --raw-output '.headers[] | " \(tojson),"' "$response_file" | ||
| printf '];\n' | ||
| } > "$OUTPUT_PATH" | ||
|
|
||
| if [[ -z "$(git status --porcelain -- "$OUTPUT_PATH")" ]]; then | ||
| echo "Removal headers are already up to date" | ||
| exit 0 | ||
| fi | ||
|
|
||
| printf '%s\n' \ | ||
| '---' \ | ||
| '"@mcansh/http-helmet": patch' \ | ||
| '---' \ | ||
| '' \ | ||
| 'Update the insecure response headers list from the OWASP Secure Headers Project.' \ | ||
| > "$CHANGESET_PATH" | ||
|
|
||
| git config user.name "github-actions[bot]" | ||
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | ||
| git add "$CHANGESET_PATH" "$OUTPUT_PATH" | ||
| git commit -m "chore(http-helmet): update removal headers" | ||
|
|
||
| if [[ -n "$remote_branch_sha" ]]; then | ||
| git push \ | ||
| --force-with-lease="refs/heads/$UPDATE_BRANCH:$remote_branch_sha" \ | ||
| origin \ | ||
| "HEAD:refs/heads/$UPDATE_BRANCH" | ||
| else | ||
| git push \ | ||
| --set-upstream origin \ | ||
| "HEAD:refs/heads/$UPDATE_BRANCH" | ||
| fi | ||
|
|
||
| existing_pr=$(gh pr list \ | ||
| --repo "$GITHUB_REPOSITORY" \ | ||
| --base "$BASE_BRANCH" \ | ||
| --head "$UPDATE_BRANCH" \ | ||
| --state open \ | ||
| --json number \ | ||
| --jq '.[0].number // empty') | ||
|
|
||
| if [[ -n "$existing_pr" ]]; then | ||
| gh pr view "$existing_pr" \ | ||
| --repo "$GITHUB_REPOSITORY" \ | ||
| --json url \ | ||
| --jq '"Updated pull request: \(.url)"' | ||
| exit 0 | ||
| fi | ||
|
|
||
| printf -v pr_body '%s\n\n%s' \ | ||
| 'Updates `packages/http-helmet/src/removal-headers.ts` from the OWASP Secure Headers Project.' \ | ||
| 'Generated automatically by `.github/workflows/update-removal-headers.yml`.' | ||
|
|
||
| gh pr create \ | ||
| --repo "$GITHUB_REPOSITORY" \ | ||
| --base "$BASE_BRANCH" \ | ||
| --head "$UPDATE_BRANCH" \ | ||
| --title "chore(http-helmet): update removal headers" \ | ||
| --body "$pr_body" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| export const removalHeaders = [ | ||
| "$wsep", | ||
| "Host-Header", | ||
| "K-Proxy-Request", | ||
| "Liferay-Portal", | ||
| "OracleCommerceCloud-Version", | ||
| "Pega-Host", | ||
| "Powered-By", | ||
| "Product", | ||
| "Server", | ||
| "SourceMap", | ||
| "X-AspNet-Version", | ||
| "X-AspNetMvc-Version", | ||
| "X-Atmosphere-error", | ||
| "X-Atmosphere-first-request", | ||
| "X-Atmosphere-tracking-id", | ||
| "X-B3-ParentSpanId", | ||
| "X-B3-Sampled", | ||
| "X-B3-SpanId", | ||
| "X-B3-TraceId", | ||
| "X-BEServer", | ||
| "X-Backside-Transport", | ||
| "X-CF-Powered-By", | ||
| "X-CMS", | ||
| "X-CalculatedBETarget", | ||
| "X-Cocoon-Version", | ||
| "X-Content-Encoded-By", | ||
| "X-Datadog-Origin", | ||
| "X-Datadog-Parent-Id", | ||
| "X-Datadog-Sampling-Priority", | ||
| "X-Datadog-Tags", | ||
| "X-Datadog-Trace-Id", | ||
| "X-DiagInfo", | ||
| "X-Envoy-Attempt-Count", | ||
| "X-Envoy-External-Address", | ||
| "X-Envoy-Internal", | ||
| "X-Envoy-Original-Dst-Host", | ||
| "X-Envoy-Upstream-Service-Time", | ||
| "X-FEServer", | ||
| "X-Framework", | ||
| "X-Generated-By", | ||
| "X-Generator", | ||
| "X-Gitlab-Meta", | ||
| "X-Jitsi-Release", | ||
| "X-Joomla-Version", | ||
| "X-Kong-Admin-Latency", | ||
| "X-Kong-Client-Latency", | ||
| "X-Kong-Proxy-Latency", | ||
| "X-Kong-Request-Id", | ||
| "X-Kong-Response-Latency", | ||
| "X-Kong-Third-Party-Latency", | ||
| "X-Kong-Total-Latency", | ||
| "X-Kong-Upstream-Latency", | ||
| "X-Kong-Upstream-Status", | ||
| "X-Kubernetes-PF-FlowSchema-UI", | ||
| "X-Kubernetes-PF-PriorityLevel-UID", | ||
| "X-LiteSpeed-Cache", | ||
| "X-LiteSpeed-Purge", | ||
| "X-LiteSpeed-Tag", | ||
| "X-LiteSpeed-Vary", | ||
| "X-Litespeed-Cache-Control", | ||
| "X-Mod-Pagespeed", | ||
| "X-Nextjs-Cache", | ||
| "X-Nextjs-Matched-Path", | ||
| "X-Nextjs-Page", | ||
| "X-Nextjs-Redirect", | ||
| "X-OWA-Version", | ||
| "X-Old-Content-Length", | ||
| "X-OneAgent-JS-Injection", | ||
| "X-Page-Speed", | ||
| "X-Php-Version", | ||
| "X-Powered-By", | ||
| "X-Powered-By-Plesk", | ||
| "X-Powered-CMS", | ||
| "X-Redirect-By", | ||
| "X-Server-Powered-By", | ||
| "X-SourceFiles", | ||
| "X-SourceMap", | ||
| "X-Turbo-Charged-By", | ||
| "X-Tyk-Trace-Id", | ||
| "X-Umbraco-Version", | ||
| "X-Varnish-Backend", | ||
| "X-Varnish-Server", | ||
| "X-Woodpecker-Version", | ||
| "X-dtAgentId", | ||
| "X-dtHealthCheck", | ||
| "X-dtInjectedServlet", | ||
| "X-ruxit-JS-Agent", | ||
| ]; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { removalHeaders } from "./removal-headers.js"; | ||
| import { removeInsecureHeaders } from "./utils.js"; | ||
|
|
||
| describe("removeInsecureHeaders", () => { | ||
| it("removes every configured insecure header", () => { | ||
| let responseHeaders = new Headers(); | ||
|
|
||
| for (let header of removalHeaders) { | ||
| responseHeaders.set(header, "exposed"); | ||
| } | ||
|
|
||
| let headers = removeInsecureHeaders(responseHeaders); | ||
| let remainingInsecureHeaders = removalHeaders.filter((header) => | ||
| headers.has(header), | ||
| ); | ||
|
|
||
| expect(removalHeaders.length).toBeGreaterThan(0); | ||
| expect(remainingInsecureHeaders).toStrictEqual([]); | ||
| }); | ||
|
|
||
| it("preserves headers that are not configured for removal", () => { | ||
| let responseHeaders = new Headers({ | ||
| "Cache-Control": "max-age=60", | ||
| "Content-Type": "text/html; charset=utf-8", | ||
| Server: "example", | ||
| }); | ||
|
|
||
| let headers = removeInsecureHeaders(responseHeaders); | ||
|
|
||
| expect(headers.get("Cache-Control")).toBe("max-age=60"); | ||
| expect(headers.get("Content-Type")).toBe("text/html; charset=utf-8"); | ||
| expect(headers.has("Server")).toBe(false); | ||
| }); | ||
|
|
||
| it("does not mutate the original headers", () => { | ||
| let responseHeaders = new Headers({ | ||
| Server: "example", | ||
| }); | ||
|
|
||
| let headers = removeInsecureHeaders(responseHeaders); | ||
|
|
||
| expect(headers).not.toBe(responseHeaders); | ||
| expect(headers.has("Server")).toBe(false); | ||
| expect(responseHeaders.get("Server")).toBe("example"); | ||
| }); | ||
| }); |
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.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Scope permissions to the job, not the workflow.
contents: writeandpull-requests: writeare declared at the workflow level. This grants both permissions to every job in the workflow, including any jobs added later, rather than only theupdatejob that needs them. Movepermissionsunderjobs.updateand add a short comment on why each scope is needed.🔒 Proposed fix
As per the static analysis hints, this triggers
excessive-permissionsat lines 9-10 andundocumented-permissionsat line 9.🧰 Tools
🪛 zizmor (1.28.0)
[error] 9-9: overly broad permissions (excessive-permissions): contents: write is overly broad at the workflow level
(excessive-permissions)
[error] 10-10: overly broad permissions (excessive-permissions): pull-requests: write is overly broad at the workflow level
(excessive-permissions)
[warning] 9-9: permissions without explanatory comments (undocumented-permissions): needs an explanatory comment
(undocumented-permissions)
🤖 Prompt for AI Agents
Source: Linters/SAST tools