Skip to content

Add a GitHub Action to render the graph as an SVG, and fix token logging, SVG injection and vulnerable dependencies - #274

Open
satishjhanwer wants to merge 8 commits into
Ashutosh00710:mainfrom
satishjhanwer:feat/github-action
Open

satishjhanwer wants to merge 8 commits into
Ashutosh00710:mainfrom
satishjhanwer:feat/github-action

Conversation

@satishjhanwer

@satishjhanwer satishjhanwer commented Nov 1, 2026 •

Copy link
Copy Markdown

Summary

This PR adds a GitHub Action that renders the activity graph to an SVG file inside the user's own workflow, so profiles don't depend on the shared Vercel instance and its rate limit. While building it I also fixed a few security issues: token exposure in logs and CI, plus markup/CSS injection in the rendered SVG. It also updates the dependencies and replaces the unmaintained node-chartist, which leaves the production dependency tree with no known vulnerabilities.

1. GitHub Action

The project only runs as an Express server today, so the only ways to use it are the shared deployment or self-hosting. The graph code itself doesn't depend on Express, so it can run directly in a workflow.

  • src/cli.ts: reads the same options as /graph from ACTIVITY_* environment variables, fetches the contributions and writes the SVG to output_path. It exits with an error instead of writing the "invalid user" SVG, so a failed run doesn't get committed as if it were a graph.
  • action.yml: composite action that sets up Node 22, installs the production dependencies and runs the CLI. Every option from Common Options is an input with the same name. Inputs are passed through env, never interpolated into the shell script.
  • README: a new "Use as a GitHub Action" section with an example workflow.

Example usage:

- uses: Ashutosh00710/github-readme-activity-graph@main
  with:
      username: ${{ github.repository_owner }}
      token: ${{ secrets.GITHUB_TOKEN }}
      output_path: profile/activity-graph.svg
      theme: github-light

The server, the /graph endpoint and the Vercel deployment are unchanged.

2. Keeping the token out of logs and CI

  • Error logging in Fetcher: the catch block logged the raw error. Axios attaches the request config, including Authorization: bearer <token>, to thrown errors, so the token could be printed into server or Actions logs. Errors are now logged by message only, and values that aren't Error objects are not logged at all.
  • CI workflow: node.js.yml interpolated secrets.TOKEN into a shell script and wrote it to a .env file on disk. It's now passed to npm test through env, and the workflow's GITHUB_TOKEN is limited to contents: read.
  • Action install step: dependencies are installed with --omit=dev --ignore-scripts. No production dependency has an install script (the only one in the tree is fsevents, a macOS-only optional dev dependency), and skipping them keeps third-party code from running in the job that holds the token.

3. Markup and CSS injection in the rendered SVG

Both issues affect the public /graph endpoint:

  • Title: custom_title, or the user's GitHub display name when no title is given, was inserted into the SVG unescaped. A crafted link such as ?custom_title=<script>... produced an SVG that runs script when opened directly. The title, and the invalid-user message, are now XML-escaped.
  • Colors: bg_color, color, title_color, line, point, area_color and border_color were written into the <style> block as #<value> without validation, so a value like fff}</style><script>... could escape the CSS. Only 3–8 digit hex codes are accepted now. Anything else falls back to the theme color, as an empty value already did.

Normal output is unchanged: the existing snapshot tests pass without updates.

4. Dependency updates and vulnerable packages

  • Updates: all dependencies are on their latest versions except the TypeScript 7 and ESLint 10 majors, which need separate migration work. Notable updates: axios 1.21.0, express 5.3.0, dotenv 18.0.9, moment 2.31.0, jest 30.5.2.
  • Replaced node-chartist: it was last published in 2017 and was the source of every remaining production vulnerability: is_js (CVE-2020-26302, no fixed release exists), form-data 2.3.3 (CVE-2025-7783), @panosoft/ramda-utils, jsdom 9, request, qs, tough-cookie and uuid. src/createChart.ts already reached into its internals, so its small amount of glue code now lives there: load Chartist into a jsdom window, render the chart, add the axis titles. It now depends directly on chartist 0.11.4 (the same version as before) and jsdom 26.1.0.
    • The chart output is byte-identical: the existing chart snapshot tests pass unchanged.
    • jsdom 26 is the newest line Jest can load; 27+ depends on ESM-only packages.
    • createGraph is now typed Promise<string> instead of any. That exposed GraphArgs.line typed as Promise<string>, and a test fixture that passed a real Promise, so its snapshot contained [object Promise]. Both are corrected. That one fixture line is the only snapshot change.
  • Non-breaking npm audit fix: patched transitive versions of path-to-regexp (used by express), brace-expansion, js-yaml, picomatch, flatted and browserslist.
  • nodemon moved to devDependencies. It is only used by the dev:start script, and in dependencies it pulled its vulnerable sub-dependencies into production installs.
  • Lockfile: regenerated; installs with both npm 10 and npm 11. The previous lockfile was rejected by npm ci on npm 11.

Result: npm audit --omit=dev reports 0 vulnerabilities (previously 10, including 3 high), and the production install drops from 259 to 163 packages. The remaining dev-only findings (jest, eslint, ts-jest) need the ESLint 10 and TypeScript 7 upgrades, which are better done separately.

Testing

  • New tests:
    • __test__/cli.test.ts: option mapping, empty inputs treated as unset, writing the SVG to a nested path, and failing without writing a file when the fetch fails.
    • __test__/fetching.test.ts: a failed request never puts the token in the logs.
    • __test__/injection.test.ts: escaping of custom_title and the display name, invalid colors rejected, valid hex colors kept.
  • The new security tests fail on the code before the fixes and pass after them.
  • The existing chart snapshots pass unchanged after replacing node-chartist, so the rendered chart is identical.
  • npm test: 19 of 20 pass. The one failure, utils.test.ts › GET /graph with correct credential, calls the live GitHub API and needs a TOKEN. It fails the same way locally without these changes, and CI provides the secret.
  • tsc --noEmit and Prettier are clean.
  • End to end: I ran the CLI the way the action does (fresh npm ci --omit=dev --ignore-scripts, invoked from another directory) against a real account. It wrote the graph correctly, escaped a <b> title, and ignored a malicious color value. An unknown username exits with code 1 and writes no file.
  • In a real workflow: the action, pinned to this PR's head commit, runs in my profile repo's stats workflow and generates the graph successfully (latest run). The log shows the token only as ***.

Axios errors carry the request config, including the Authorization header with the token, so logging the whole error object can print the token into logs.
Adds src/cli.ts, which reads the same options as the /graph endpoint from ACTIVITY_* environment variables, fetches the contributions and writes the SVG to output_path. It exits with an error instead of writing the 'invalid user' SVG, so a failed run is not committed as a graph.

action.yml wraps the CLI as a composite action. Inputs are passed through env rather than interpolated into the shell script. The README documents the action with an example workflow.
The catch block logged anything that wasn't an Error as-is, so a thrown object carrying the request config would print the Authorization header. Log a fixed message for those instead, and add a regression test that a failed request never puts the token in the logs.
- node.js.yml: pass secrets.TOKEN to npm test through env instead of echoing it into a .env file from an interpolated script, and limit the workflow's GITHUB_TOKEN to contents: read.
- action.yml: install dependencies with --ignore-scripts. No dependency needs install scripts, and skipping them keeps third-party code from running in the job that holds the token.
- The title (custom_title, or the user's GitHub display name) was inserted into the SVG unescaped, so a crafted link could make the served SVG run script when opened directly. Escape it, and the invalid-user message too.
- Color options were written into the <style> block as #<value> without checks, so a value could close the style tag. Only 3-8 digit hex codes are accepted now; anything else falls back to the theme color, as an empty value already did.
Updates to the latest versions, except the TypeScript 7 and ESLint 10 majors (axios 1.21.0, express 5.3.0, dotenv 18.0.9, moment 2.31.0, jest 30.5.2, ...). The lockfile is regenerated and installs with both npm 10 and npm 11.
- Override form-data under request to 2.5.6 (CVE-2025-7783). It comes in through node-chartist -> jsdom 9 -> request, which pins the vulnerable 2.3.x.
- Apply the non-breaking npm audit fixes (path-to-regexp under express, brace-expansion, js-yaml, picomatch, flatted, browserslist).
- Move nodemon to devDependencies. It is only used by the dev:start script and kept its vulnerable dependencies in the production install.
node-chartist was last published in 2017 and pulled in jsdom 9, request, @panosoft/ramda-utils and is_js, which has an unfixed ReDoS (CVE-2020-26302). createChart.ts already reached into its internals, so its ~100 lines of glue now live there: load Chartist 0.11.4 (the same version) into a jsdom 26 window, render, add the axis titles. The chart output is byte-identical; the existing chart snapshots pass unchanged.

- npm audit --omit=dev now reports 0 vulnerabilities, and the production install drops from 259 to 163 packages.
- The form-data override is removed, since request is no longer in the tree.
- jsdom 26 is the newest line Jest can load; jsdom 27+ depends on ESM-only packages.
- createGraph now returns Promise<string> instead of any, which exposed GraphArgs.line typed as Promise<string>. The test fixture passed a real Promise, so its snapshot contained '[object Promise]'; both are corrected.
@satishjhanwer satishjhanwer changed the title Add a GitHub Action to render the graph as an SVG, and fix token logging and SVG injection Add a GitHub Action to render the graph as an SVG, and fix token logging, SVG injection and vulnerable dependencies Nov 1, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant