expressjs

Activity

Stars and mentions · Last 30 days, Oct 5 – Nov 3

GitHub stars202
Mentions20
012243648Oct 5Oct 19Nov 3
UTC days · repositories expressjs owns1 repository

Top repositories

expressjs’s repositories ranked by new stars, as on trending.

Loading

Contributes to

Repositories outside expressjs’s account where expressjs opened or reviewed pull requests in the past 365 days.

Who noticed expressjs

Everyone who starred or forked expressjs’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

21posts
Nov 41 post
  1. I built an altar for the code my repo buried at MLH Global Hack Week (git-ofrenda + local Gemma) This is a submission for the MLH x DEV Writing Challenge What I Built git-ofrenda 🕯️🌼 is a tiny CLI that builds an altar for the code your repo buried. Every long-lived repository has a graveyard: files that handled real traffic for years, went through a hundred commits, and then disappeared in a commit called "Removed old cruft". git log still remembers them. Nobody visits. git-ofrenda digs them up: 1. It walks your git history and finds every file that was deleted and never came back (renames and resurrections don't count). 2. It pulls each file's life story from git: who added it and when, who removed it and when, the commit message that killed it, how many commits it lived through, and its last lines of code. 3. A local Gemma 3 model writes each one a short epitaph and remembrance. 4. It renders a static, candlelit altar page, one headstone per file. 5. ofrenda seance lets you ask a dead file questions. The ghost only knows its own git record. I built it in the last week of October, during MLH's Global Hack Week: Hacktoberfest season, when Halloween and Día de los Muertos were both a few days away. The idea came from that overlap: Halloween gets the séance, and Día de los Muertos gets the part I actually care about, remembering the work that came before us. Open source runs on code somebody wrote, maintained, and eventually retired. It's one Python file, standard library only, with 11 unit tests and a SKILL.md so a coding agent can run it for you. What I learned along the way - Let git own the facts, let the model own the words. Every date, count and name on a headstone is printed straight from git. Gemma only writes the epitaph and the short paragraph, and the page labels which is which. - Small models invent numbers, so check them in code. If an epitaph contains any number that isn't in the file's facts, it's rejected and retried. After three misses the headstone falls back to a plain template and says so. - Generic prompts give generic poetry. My first run gave me twelve epitaphs about "silent echoes" and "digital dust". Telling the model to play on something specific to that file (its name, its last line, its cause of death) and banning the stock phrases fixed most of it. view.js from early Express got "'Return str;' – A final, insistent plea." - The guard catches fake statistics, not fake meaning. Gemma once called a 769-line test file "small" because its last test was tiny. That's exactly why the numbers never come from the model. Demo Live altar for Express.js: aniruddhaadak80.github.io I pointed it at the Express repo: 6,183 commits and 502 files deleted and never revived. The altar shows 12 headstones, the .js files that lived through the most commits, many from Express's 2009-2011 era. !The Express altar !One headstone with its last words opened The build and a séance, as they ran in the terminal: !Build log and séance in the terminal I asked lib/express/core.js why it was let go, and it answered with its real commit message ("old cruft"), the person who removed it, the 200 days it lived and its actual last line of code, then said "the veil hides the specifics" for the part git doesn't know. That's the behaviour I wanted. Try it on your own repo: ollama pull gemma3:4b python3 ofrenda.py list ~/src/your-repo # what did it bury? python3 ofrenda.py build ~/src/your-repo --model gemma3:4b # build the altar python3 ofrenda.py seance ~/src/your-repo path/to/dead/file.js --model gemma3:4b {% github aniruddhaadak80/API_projects %} Code is in the git-ofrenda/ folder (MIT). Partner Technologies Gemma (Google DeepMind) Gemma 3 4B Instruct writes every epitaph and speaks at every séance. I ran it as a 4-bit GGUF (ggml-org/gemma-3-4b-it-GGUF, Q4_K_M, about 2.5 GB) through llama.cpp's llama-server, and the same code also talks to Ollama's native API. How it's wired: - The prompt hands Gemma a facts block for one file and asks for exactly two lines, EPITAPH: and REMEMBRANCE:. - Code validates the shape, rejects any number not present in the facts, and retries up to three times before falling back to a template. - For the séance, the file's record is the model's whole world, and it's told to say "the veil hides it" for anything outside it. What I measured on a machine with 2 ARM CPU cores, 7 GB RAM and no GPU: | | Gemma 3 4B (Q4_K_M) | Gemma 3 1B (Q4_K_M) | |---|---|---| | Altar of 12 headstones | 317 s (≈26 s per file) | 118 s | | Fell back to the template | 0 of 12 | 2 of 12 | | Generation speed | ≈8 tokens/s | ≈27 tokens/s | The 1B model is three times faster, but its epitaphs were noticeably more generic, so 4B is my default. Five minutes for an altar you build once per repo is fine. Working with Gemma locally was the right call for this tool: a repo's history is full of names, internal commit messages and code that was removed for a reason. With an open-weight model on your own machine, nothing leaves it, there's no API key, and it keeps working offline. GitHub The code lives on GitHub, git itself does all the archaeology (git log --diff-filter=D, git show <death>^:path), and the demo altar is hosted on GitHub Pages as a single static HTML file. Hackathon Experience I did this one solo and fully online, during MLH's Global Hack Week: Hacktoberfest in October 2026. Most days that month started with the "Today in GHW" livestream, and the sessions that shaped this project were the Gemma session with Google DeepMind and the one on agent skills and MCP servers. The first is why I tried running Gemma on a plain CPU instead of reaching for an API; the second is why git-ofrenda ships with a SKILL.md. What I'll remember most is the moment it first worked: opening lib/express/core.js on the altar. Born November 2009, 222 commits, 6 keepers, gone in June 2010 with "Removed old cruft", and its last lines still there, wiring a request to a Router. One of the earliest pieces of a framework a huge part of the Node.js world was built on, and most people using Express today have never seen it. git-ofrenda doesn't bring that code back. It just lights a candle for it. If you run it on your own repo, I'd love to see what your altar looks like. Drop it in the comments. 🕯️

  2. The Node.js Brotli flaw: your server decompresses whatever APIs send back On October 21, Node.js fixed a flaw in its Brotli decoder, CVE-2026-111497. I went through the Node images in my projects. Two Next.js frontends are built on a tag that still ships Node 22.21.1. That tag will never get the fix. > TL;DR: a crafted Brotli stream can make Node write outside the memory it reserved. The process can crash, leak memory, or run an attacker's code. You are exposed if your server decompresses Brotli that comes from outside. Node's fetch, axios and Express 5 all do it by default. Move to Node 22.23.4, 24.22.1 or 26.12.1. Node 18, Node 20 and some Docker tags will never be fixed. Versions and source status: snapshot as of October 31, 2026. Check the latest fixes before deploying. This article is for Node.js developers and for teams that ship Node in containers. You do not need to know Brotli to follow it. Who decompresses Brotli without knowing it The flaw has one condition. Your application must decompress Brotli data supplied by someone else. So the real question is simple. Which code on your server decompresses Brotli? I read the source code of Node, undici, axios and body-parser. The table sums up what each one does. The fetch case is the quietest one. Node built it on undici, the HTTP library of the Node.js project. Over HTTPS, fetch asks the server for Brotli on its own, with an accept-encoding header that includes br. Then it decompresses the response without asking you. Changing that header does not protect you. In undici's code, the decoder depends on the response's Content-Encoding header. A hostile server can send you Brotli even if you did not ask for it. The risk comes from URLs you do not control. Think of a webhook entered by a customer, a link preview, an RSS feed or an image to download. A compromised third-party API is enough too. For incoming requests, Express 5 relies on body-parser 2. That version decompresses bodies sent with Content-Encoding: br. So any client can push a Brotli stream at you. Express 4 uses body-parser 1, which does not know Brotli. !Two paths lead to Node's Brotli decoder Two paths lead to the decoder. The responses your server receives, and the bodies it accepts. You wrote neither. An image that only builds a frontend receives no traffic. It still needs the update, but it is not the priority. What Node.js fixed on October 21 On October 21, Node.js shipped three security releases: 22.23.4, 24.22.1 and 26.12.1. The advisory lists ten flaws. Four are rated "high". Only one can lead to code execution, the one in the Brotli decoder. A server that compresses with Brotli says so with the Content-Encoding: br header. The client must then decompress the response before reading it. That is the decoder's job. The decoder reads the stream piece by piece. When it hit an invalid section, it thought it was just missing a few bytes. It waited for more instead of rejecting the stream. Meanwhile, it kept storing the next bytes in a small eight-byte buffer. It did not stop at the edge. So it wrote into the memory next to it. This is called an out-of-bounds write. Node lists three possible effects. The process can crash. It can leak part of its memory. Or the attacker can run their own code inside your Node process. Node rates the flaw "high". The CVE record, written by Google, scores it 9.2 out of 10. The NVD, the US vulnerability database, scores it 9.8. The bug comes from Google's Brotli library. Google shipped the fixed versions the same day: 1.0.10, 1.1.1 and 1.2.1. But Node ships its own copy of Brotli. I checked with ldd in two official images. The node binary loads no Brotli library from the system. So updating the packages in your image changes nothing for Node. It is the same trap as Go's eMudhra fix, which I covered yesterday. Confirmed exploitation since October 27 On October 22, the CVE record still said "Exploitation: none". CISA added the flaw to its KEV catalog on October 27. That catalog records confirmed exploitation. The assessment changed to "active" the next day. On October 30, Singapore’s Cyber Security Agency also reported a public proof of concept. These sources describe successive states. They do not give an account of a specific attack here. This snapshot ends on October 31, 2026. My lab: 14 FROM node lines, three versions that will never be fixed I listed the FROM node: lines in every Dockerfile of my projects. I count 14 of them, in 11 files. For each tag, I read Docker Hub, then ran node --version in today's image. The tag that surprised me is node:22-alpine3.21. It looks like it follows Node 22. In fact, it ships Node 22.21.1, and it has not moved in a year. Alpine is a very small Linux distribution. The alpine3.21 suffix pins its version. The team behind the Node images dropped Alpine 3.21 on December 4, 2025. Today it only builds the Alpine 3.23 and 3.24 variants. Since then, that tag gets no new Node version at all. Nothing warns you. The build passes and the image starts. One of the two repositories does have a Dependabot that watches Docker images. In May, it proposed moving to node:25-alpine3.21. It kept the suffix, so it stayed inside the frozen family. Node 25 reached end of life three weeks later, on June 1. node:18-alpine and node:20-alpine are frozen for another reason. Node 18 has been end of life since April 30, 2025, Node 20 since April 30, 2026. Their copy of Brotli is version 1.1.0, inside the affected range. Node's advisory says it plainly. These official releases remain vulnerable because their release lines received no fix. One last detail, on my laptop. The cached node:24-alpine image dates from August 3. It holds Node 24.19.0, without the fix. A build that reuses that copy makes a vulnerable image, even with an up-to-date tag on Docker Hub. Then I searched my code for anything that decompresses Brotli. No service calls the Brotli functions of zlib. None uses Express. The two Next.js frontends make no server-side fetch in production. Their API calls leave from the browser. A single route makes a server-side fetch. It only turns on in local development, toward a storage service I control. So my real exposure is low. But a grep does not see what my dependencies do. And the next server-side fetch will arrive without anyone rereading the FROM line. The Brotli version number proves nothing Node shows the version of its libraries in process.versions. I compared two Node 22 images, one vulnerable and one fixed: docker run --rm node:22-alpine3.21 node -p "process.version + ' brotli ' + process.versions.brotli" # v22.21.1 brotli 1.1.0 docker run --rm node:22-alpine node -p "process.version + ' brotli ' + process.versions.brotli" # v22.23.4 brotli 1.1.0 Both show Brotli 1.1.0. Node applied Google's fix to its copy without changing that number. Node 24 is the same, with 1.2.0 before and after. Compare the Node version with 22.23.4, 24.22.1 or 26.12.1 instead. The fix also adds a constant to zlib.constants. In my tests, it only exists in the fixed versions. It makes a quick check in a running container: docker exec my-container node -p "process.version + ' ' + ('BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH' in require('zlib').constants)" # v22.23.4 true (fixed) # v22.21.1 false (vulnerable) What to change If you cannot deploy today, two settings reduce the risk. With Express 5, the inflate: false option rejects compressed bodies with a 415 error: app.use(express.json({ inflate: false })) // a body sent with Content-Encoding: br gets a 415 error With axios, the decompress: false option turns decompression off. Your code then gets the compressed bytes, and handling them is up to you. For fetch, I found no matching setting. Only a Node update protects it. The checklist > A Node fix only protects the container where it actually runs. - [ ] List every FROM node: line in your Dockerfiles - [ ] Spot end-of-life versions: Node 18, Node 20 and older - [ ] Spot tags tied to an Alpine version that is no longer built, such as alpine3.21 - [ ] Run node -p process.version in every production container - [ ] Do not trust process.versions.brotli - [ ] Look for server-side fetch and axios calls to URLs you do not control - [ ] Look for Express 5 and direct calls to the Brotli functions of zlib - [ ] Rebuild with --pull, then redeploy The takeaway The Brotli flaw does not need you to call Brotli yourself. fetch, axios and Express 5 do it for you, on data that comes from outside. And a Docker tag can look current while it ships a version that is a year old. Open your Dockerfiles today. Look for Node 18 and 20, and for tags that pin an Alpine version. Nobody will update them for you. Want a review of your images and your build pipeline? See my DevSecOps pipeline offer, or let's talk. Dossier DevSecOps et IA Continuer sur ce sujet Voir le dossier complet, puis deux retours directement liés et le service associé. --- Sources: Node.js, October 21, 2026 security releases · Node.js 22.23.4 release notes · CVE-2026-111497 record · NVD, CVE-2026-111497 · CISA KEV catalog · Singapore CSA, alert AL-2026-154 · Brotli 1.2.1 release notes · undici 6.29.1, fetch source · body-parser 2.4.0, body reading · axios HTTP adapter · Node.js zlib documentation · Node.js release schedule · docker-node, Alpine 3.21 removal · Docker Hub, node image · docker build documentation

  3. @Madhav_XO If you’re comfortable with JavaScript, I’d start with Express: expressjs/express. Pick one case in test/app.router.js, then trace it into lib/application.js. That gives you one request path to understand before exploring the rest.

    10 views
  4. git-ofrenda: an altar for the code your repo buried, with epitaphs by Gemma on a plain CPU 🕯️ This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine What I Built git-ofrenda 🕯️🌼 builds an ofrenda for the code your repo buried. Every repository that lives long enough has a graveyard. Files that handled real traffic for years, got touched by a hundred commits and half a dozen people, and then vanished in a commit called "Removed old cruft". git log still remembers them. Nobody visits. git-ofrenda digs them up: 1. It walks your git history and finds every file that was deleted and never came back. 2. It collects the facts of each one's life from git: when it was born and who added it, when it died and who removed it, the commit message that killed it, how many commits it lived through, who tended it, and its last lines of code. 3. A local open-weight model (Gemma 3, through Ollama or llama.cpp) writes each one an epitaph and a short remembrance. 4. It builds an altar: one static page with a candlelit headstone per file. 5. And if you want to ask a dead file something, ofrenda seance lets you talk to it. The ghost only knows its own record. That's my reading of the theme from both sides: Halloween gets the séance, and Día de los Muertos gets the part I care about more, remembering the work that came before. Open source runs on code that someone wrote, maintained, and eventually retired. Those files deserve better than a line in git log --diff-filter=D. It's a single Python file with no dependencies beyond the standard library, and it ships with a SKILL.md, so a coding agent can run it for you too. I build agent skills, so that was the natural shape. Demo The altar for Express.js: aniruddhaadak80.github.io I pointed it at the Express repository: 6,183 commits, and 502 files deleted and never revived. The altar has 12 headstones, the .js files that lived through the most commits. A lot of them are from Express's 2009-2011 era: lib/express/core.js, request.js, view.js, a plugins/ folder, a Collection class, and tests for a Jade view engine. !The Express altar Each headstone keeps the model's words and git's facts apart. The italic epitaph and the short paragraph are Gemma's. The dates, commit count, number of keepers, cause of death and last words are printed straight from git. !One headstone with its last words opened Here's the full build and a séance, as it ran: !Build log and séance in the terminal Code {% github aniruddhaadak80/API_projects %} git-ofrenda lives in the git-ofrenda/ folder (MIT). ofrenda.py the CLI: list, build, render, seance (stdlib only) SKILL.md agent skill: when to use it, how to read the output test_ofrenda.py 11 unit tests index.html the live Express altar express/ raw souls.json, OFRENDA.md, build log, séance transcript Try it on your own repo: ollama pull gemma3:4b python3 ofrenda.py list ~/src/your-repo # what did it bury? python3 ofrenda.py build ~/src/your-repo --model gemma3:4b # build the altar python3 ofrenda.py seance ~/src/your-repo path/to/dead/file.js --model gemma3:4b It also works with llama.cpp's llama-server, LM Studio or vLLM: pass --url http://localhost:8080/v1. With no model at all, --no-model still builds a facts-only altar. How I Built It The open pieces - Gemma 3 4B Instruct, an open-weight model, as a 4-bit GGUF (ggml-org/gemma-3-4b-it-GGUF, Q4_K_M, 2.49 GB). - llama.cpp (llama-server, build b11913) for local inference on plain CPU. The same code also talks to Ollama's native API. - git itself, which is open source and is the real archivist here. Git does the remembering, Gemma does the mourning The rule I followed: anything that can be a fact comes from git, and the model only writes words. - git log --diff-filter=D finds deletions. Renames aren't deaths (git's rename detection handles that). Files deleted and later re-added are alive, so they're skipped. Lockfiles, images and minified bundles are ignored. - For each file it reads the first commit that added it, every commit that touched it, and its content one commit before it died (git show <death>^:path). The last 12 non-blank lines become its "last words". - Files are ranked by how many commits they lived through, with at most two per burial commit, so one big cleanup can't take every headstone. Keeping a small model honest The prompt gives Gemma the facts block and asks for exactly two lines, EPITAPH: and REMEMBRANCE:. Then code checks the answer: - Wrong shape (a list of "options", a missing line, a ramble) → retry. - Any number that isn't in the file's facts → reject and retry. A model that says a file "fixed 47 bugs" when git never said so doesn't get a headstone. - Three misses → a plain factual template, and the card says written by template. My first prompt got me twelve near-identical epitaphs about "silent echoes" and "digital dust". Telling the model to play on something particular to this file (its name, its last lines, the words of its cause of death) and banning those stock phrases helped a lot. Gemma started quoting the code: view.js got "'Return str;' – A final, insistent plea.", and the view plugin, whose final lines hand null to a callback, got "'Null' was its last word, a clean, decisive end." The séance seance gives the model the file's record as its whole world and tells it to say "the veil hides it" for anything the record doesn't answer. I asked lib/express/core.js why it was let go: > Oh, my dear, it was a rather abrupt farewell, wasn't it? Tj Holowaychuk simply deemed me "old cruft" – a terribly blunt assessment, if you ask me. I suppose I did spend a good 200 days diligently handling requests, though my last lines were quite straightforward: if (typeof (body = (new Router(request)).route()) === 'string') request.respond(200, body). The veil hides the specifics of why they chose that particular moment, but I don't truly regret my work, it was a simple, necessary task. Every fact in that answer checks out against git: the commit message, who removed it, the 200 days, and that line really is in its final version. What I measured All on a machine with 2 ARM CPU cores, 7 GB RAM and no GPU: | | Gemma 3 4B (Q4_K_M) | Gemma 3 1B (Q4_K_M) | |---|---|---| | Altar of 12 headstones | 317 s (≈26 s per file) | 118 s | | Fell back to the template | 0 of 12 | 2 of 12 | | Generation speed | ≈8 tokens/s | ≈27 tokens/s | | One séance answer | 34 s | not tested | The 1B model is three times faster, but its epitaphs were generic, so 4B is what I'd use. Five minutes for an altar is fine for something you run once per repo. Where it still gets things wrong The number guard catches invented statistics, not invented meaning. On Express, Gemma 4B called test/response.test.js a "small file" that "served only to declare 'hey'". It was 769 lines with 103 commits; it just happened to end with a tiny res.send('<p>hey</p>') test. That's exactly why the dates and counts on every headstone come from git and the poetry is labelled as the model's. Tests (python3 -m unittest -v test_ofrenda.py, 11 passing) build a throwaway repo with a rename, a resurrection and a binary file, and check the archaeology, the parser, the number guard, the retry-then-template path against a fake OpenAI-style server and a fake Ollama server, and an unreachable server. Why Does Open Innovation Matter? Your git history is private, and this keeps it that way. A repo's history is full of names, internal commit messages and old code that was removed for a reason. An open-weight model running on your own machine means git-ofrenda can read all of it without uploading anything. I went one step further: the HTTP client ignores system proxy settings, so requests go straight to the localhost URL you give it. It works with the lights off. After the model is downloaded once, building an altar or holding a séance needs no internet and no API key. It runs on whatever you have. Gemma 3 is open-weight and comes in sizes from 1B up, and llama.cpp and Ollama run it on a laptop CPU. I measured it on two CPU cores with no GPU. A closed API would have given me nicer prose and a bill per repo, and it would stop working the day the service changes its terms. It fits the theme. This is a tool for honouring code that open-source maintainers wrote and retired. It would feel wrong if remembering their work depended on a service that might disappear too. The model, the runtime, git, and git-ofrenda itself are all open, so the altar can outlive any one of them. The Spirit of It The moment it worked for me was opening lib/express/core.js on the altar: born November 2009, 222 commits, 6 keepers, gone in June 2010 with "Removed old cruft", and its last lines still there, wiring request.trigger('request') to a Router. That's one of the earliest pieces of a framework that a huge part of the Node.js world was built on, and most people using Express today have never seen it. git-ofrenda doesn't make that code come back. It just lights a candle for it. Prize Categories - Best Use of Gemma: Gemma 3 4B Instruct (open weights, GGUF via llama.cpp) writes every epitaph and speaks at every séance. Gemma 3 1B was measured as the fast option.

    1 reaction
  5. 【要対応】Node.jsの脆弱性10件が修正。Claude Code・MCP利用者が今すぐ確認すべきこと TL;DR 2026年10月21日、Node.jsの深刻な脆弱性を含む10件が修正された。 修正版は v22.23.4 / v24.22.1 / v26.12.1。該当する古いNodeを使っているなら更新が必要 Nodeを更新しただけでは終わらない。 自分のMacでは、修正前のNodeで動くプロセスが19個残っていた Claude Code本体、MCPサーバー、Cursor CLI、公開アプリでは確認・更新する場所が異なる 本番のDockerイメージも、再ビルド・再デプロイしない限り古いまま残る この記事では、今すぐ確認する手順、実際に対応した結果、同じことを繰り返さないための仕組みをまとめた Node を上げた。19個、直っていなかった 10月23日、仕事のチャットに「Node.js が危ないのでアップデート必須。Claude Code を使っている人はガッツリ関係する。会社単位じゃなく個人単位で」と連絡が回ってきた。 自分も最初はこう思った。 「Claude Code って Node で動いてるんだっけ?」 「本体は勝手に更新されるんでしょ?」 「GitHub も見るの? GCP は? Vercel は?」 調べてみたら、答えはどれも「半分合ってる」だった。 自分の Mac を見たら、Node は修正前の 26.10.0 のまま。brew upgrade node で 26.12.1 に上げた。これで終わり、と思った。 念のためプロセスを数えた。 修正前の Node で動き続けているものが、19個あった。 Astro の dev サーバーが10個。Next.js が4個。Claude Code が裏で起動した MCP サーバーが5個。いちばん古いものは1週間前から動いていた。しかも最初に打ったコマンドでは、そのうち3個が見えていなかった。 極めつけは、この調べものに使っていた Claude Code 自身だ。スマホから使うために Mac mini で常駐させていた Remote Control の親プロセスが、1週間前の 2.1.292 のまま動いていた。claude update で新しい版を入れても、そこから起動するセッションは古いままだった(後述)。 アップデートは、インストールした時点では終わらない。この記事は、その話だ。 自分はどれ? 3つの質問で判定する 質問 はい → いいえ → ① Claude Code・Cursor・Codex などの AI ツールを使っている? A に該当。次の質問へ 次の質問へ ② ターミナルで node -v を打つと、バージョンが出る? B に該当。次の質問へ 次の質問へ(ただしアプリ同梱の Node は残る。後述) ③ Node で作ったアプリやサーバーを、ネットに公開している? C に該当 — そのうえで、node -v や本番の Node が v18・v20・v23・v25 なら D も当てはまる。修正版が出ない。 区分 誰 やること 所要 A AI ツールを使っている人 Claude Code・Cursor CLI・デスクトップアプリを最新に 5分 B PC に Node が入っている人(MCP サーバー、npx、npm で入れた CLI、dev サーバー) Node を更新して、動いているものを再起動 10〜20分 C Node のアプリを公開している人 本番の実行環境を更新して再デプロイ 環境しだい D Node 20 以前などを使っている人 22 / 24 系に乗り換え 半日〜 Claude Code 本体だけ使っている人は、まず A。たぶんこの層がいちばん多い。 ただ、Claude Code を入れるときに「Node も入れてね」と言われて Homebrew で入れた人は多いはず。その場合は B も当てはまる。node -v を一回打ってみてほしい。 結局、どこを見ればいいのか 見る場所は3層ある。 層 何を見る 見落としやすいもの ① PC(持っている台数ぶん) Claude Code の版、node -v、動いている Node、アプリ同梱の Node 普段使わないPC、起動しっぱなしの dev サーバー ② リポジトリ Dockerfile の FROM node:..、.nvmrc / .node-version、package.json の engines、GitHub Actions の node-version FROM node:20 のまま、node-version: 24.14.0 みたいな細かい固定 ③ 本番 実際に動いているランタイムの版 「Dockerfile は直したけど、まだデプロイしてない」 ②はリポジトリの置き場所でこう打てば一覧が出る。 grep -rnE "FROM node|node-version|\"node\":" \ --include=Dockerfile* --include=.yml --include=.yaml \ --include=package.json --include=.nvmrc --include=.node-version \ --exclude-dir=node_modules . 自分のリポジトリ(10本ちょっと)でやったら、こうなった。 本番が Cloud Run(自前の Dockerfile)で FROM node:20-alpine のもの:1本。修正が出ない系列。最優先 本番が Cloud Run で FROM node:22-alpine のもの:3本。イメージを作り直して再デプロイ CI で node-version: 24.14.0 に固定しているもの:1本。本番は Cloudflare Workers なので Node では動いていない。CI だけ上げればいい Python のサービス、静的サイト(Firebase Hosting):本番は関係なし CI(GitHub Actions)は、外から来たデータを展開する場面が少ないので優先度は低い。でも node-version の固定は、ついでに上げておく。 そもそも何が直ったのか Node.js の公式発表では、10件のうち4件が深刻度「高」だった。 CVE ざっくり言うと 危ない場面 CVE-2026-111497 Brotli(圧縮形式の一つ)を展開するとき、メモリの範囲外に書き込める 外から来た Brotli データを Node が展開するとき CVE-2026-106069 0127.0.0.1 みたいな変な書き方の IP アドレスで、TLS 証明書のチェックがずれる 接続先のホスト名を外部入力で決めていて、かつ通信経路を握られているとき CVE-2026-105483 全角数字の IP アドレスで、TLS 証明書のチェックがずれる(24 / 26 系だけ) 同上 CVE-2026-105482 dns.resolve が名前の途中の NUL 文字(\0)で切れる 「許可リストで名前をチェックしてから名前解決する」作りのとき 残り6件は中・低。HTTP の keep-alive、crypto.randomUUID()、権限モデルまわりなど。 修正版は v22.23.4 / v24.22.1 / v26.12.1。回避策は出ていない。上げるしかない。 Brotli のバグは、理屈の上では任意のコード実行までつながる種類のものだ。Node の発表でも「リモートからのコード実行を許す」と書かれている。ただ、10月23日時点で GitHub に公開されている PoC と解析を見ると、5本のうち4本は「リモートからのコード実行は成立していない。実際にはプロセスを落とせる(DoS)」という結論だった(条件を固めたローカル環境でなら実行できた、という報告はある)。落とせるだけでも、サーバーなら十分困る。 どんな場合に危ないのか ここがいちばん混乱しているところだと思う。 TLS と DNS の3件は、条件がかなり狭い。「接続先を外部入力で決めるサーバーを作っている」「経路上に攻撃者がいる」くらいの前提が要る。普通に使っているだけなら、ほぼ当たらない。 問題は Brotli の1件だ。こっちは入口が広い。 当たる:Node が外部のサイトやAPIを fetch() する Node の fetch() は、HTTPS のとき「Brotli で圧縮して送ってくれていいよ」とサーバーに伝える。サーバーが Brotli で返してきたら、中身を自動で展開する。 つまり、相手のサーバーが悪意を持っていたり、乗っ取られていたりしたら、fetch() しただけで細工したデータが届く。 当てはまるもの: URL を取りに行く MCP サーバー(Web取得、ブラウザ操作、スクレイピング系) 外部 API を叩くスクリプトやバッチ Claude Code に作らせた「URLを入れると要約するツール」みたいなもの 当たる:外からのリクエスト本文を展開するサーバー Express 5(body-parser 2.x)は、express.json() などのパーサーを使っていると、Content-Encoding: br のリクエスト本文を既定で展開する。 Express で API を公開しているなら、C に該当する。 ちなみに公開されている PoC(攻撃の実証コード)の一部も Express を使っている。ただし受け取った本文を自前の処理で Brotli 展開する作りで、body-parser の既定の動作を狙ったものではない。 当たる:受け取ったファイルを zlib で展開する アップロードされた .br ファイルを展開する、みたいな処理。これも外から来たデータの展開だ。 ほぼ当たらない:自分のPCの中だけで完結するもの localhost でしか見ていない dev サーバーが、外部と通信していないなら、細工したデータが届く経路がない。 ただし「dev サーバーの中で外部 API を fetch() している」なら当たる。Next.js のサーバー側で外部APIを呼んでいる、とか。けっこうある。 当たらない:Node を入れているだけ インストールされているだけで、何も動いていなければ攻撃は届かない。ただ、MCP サーバーは Claude Code を開くたびに裏で起動する。「動かしていないつもり」が一番あぶない。 Claude Code 本体の話:Node じゃなくて Bun 本体は Node を使っていない Claude Code の公式ドキュメントには、「インストールされた claude は Node を呼び出さない」とある。npm で入れた場合も、中身は同じネイティブバイナリだ。 だから「Node を更新すれば Claude Code 本体が直る」は、正しくない。 中の Bun にも、同じ Brotli のコードが入っていた Claude Code のネイティブ版は Bun で作られている。 その Bun にも、同じ Brotli ライブラリが入っていた。Bun の issue #46352 では「全リリースが影響を受ける」とされ、10月22日に修正がマージされた(日本時間11時ごろ)。修正を含む Bun v1.4.4 のリリースは同日19時ごろだ。 じゃあ Claude Code 本体で攻撃が届くのか。ここは分からない。Claude Code の中で、外から来た Brotli データを Bun が展開する場面があるのか。Anthropic は何も公表していない。 2.1.308 に上げた。直ったかは分からない 自分の Mac に入っていたのは 2.1.306。公開は10月21日(日本時間12時ごろ)で、Bun の修正より前だ。 claude update を打ったら 2.1.308 になった。 $ claude update Current version: 2.1.306 Updating to 2.1.308... Successfully updated from 2.1.306 to version 2.1.308 2.1.308 の公開は10月22日の13時半ごろ。Bun の修正マージよりは後、Bun v1.4.4 のリリースよりは前。リリースノートは「Bug fixes and reliability improvements」の1行だけだった。 なので言えるのは「最新にしておくのが、いまできる最善」まで。直ったとは言えない。 「本体は勝手に更新される」は本当か 入れ方による。公式ドキュメントを読むと、こうなっている。 入れ方 自動で更新される? 手で上げるコマンド 公式インストーラー(curl ... install.sh) される claude update npm される(書き込み権限があれば) npm install -g @anthropic-ai/claude-code@latest Homebrew(brew install --cask claude-code) 既定ではされない brew upgrade claude-code WinGet 既定ではされない winget upgrade Anthropic.ClaudeCode apt / dnf / apk されない OS のアップデートで Homebrew と WinGet は、CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE=1 を設定すると Claude Code が代わりに更新してくれる。 自分の Mac は公式インストーラー版で、9/26・10/7・10/16・10/22 と更新されていた。 それでも 2.1.306 で止まっていた。自動更新は「起動したときと、動いている間にときどき」見に行って、次に起動したときに切り替わる仕組みだからだ。急ぐなら claude update を手で打つ。 ちなみに配布には latest と stable(ふだんは約1週間遅れ)の2つのチャンネルがある。今回は10月22日の時点で、stable も 2.1.308 になっていた。 逆に Node は、誰も勝手に更新してくれない。Homebrew で入れたなら、brew upgrade を打つまで永遠に古いままだ。 手順 まず一発で確認する PC ごとに、これをターミナルに貼る。普段使わない PC も忘れずに。 Mac / Linux: echo "== Claude Code"; claude --version 2>/dev/null || echo "入っていない" brew list --cask 2>/dev/null | grep '^claude-code' && echo "↑ Homebrew 版。既定では自動更新されない" echo "== Node"; which -a node 2>/dev/null; node -v 2>/dev/null || echo "入っていない" echo "== Cursor CLI"; cursor-agent --version 2>/dev/null || echo "入っていない" echo "== 動いている Node"; ps -axo pid,lstart,command | grep -E '[n]ode |[n]ext-server' | cut -c1-150 Windows(PowerShell): "== Claude Code"; claude --version "== Node"; where.exe node; node -v "== 動いている Node"; Get-Process node -ErrorAction SilentlyContinue | Select-Object Id, StartTime, Path 最後の「動いている Node」の読み方: 起動時刻が Node をアップデートする前 → 古い Node で動いている。再起動する パスが /Applications/〜.app/ の中 → アプリが同梱している Node。アプリの更新を待つ パスが Homebrew の Caskroom の中 → Homebrew で入れたツール(Cursor CLI など)が同梱している Node。brew upgrade --cask <名前> で上がることがある コマンドの引数に node という文字を含むだけの別のプロセスも出てくる。パスを見て判断する next-server → Next.js の dev サーバー。名前に node が付かないので、grep node だけだと見逃す。自分はこれで3個見逃した 「入っていない」と出ても、それは PATH の上に無いというだけだ。アプリ同梱の Node、Docker、本番は別に見る(下の「Node を同梱しているアプリ」と C)。 A:AI ツール本体 claude update claude --version # 2.1.308 以降を推奨(修正が入ったかは未公表) Homebrew で入れた人は brew upgrade claude-code(latest 版なら claude-code@latest)、WinGet なら winget upgrade Anthropic.ClaudeCode。 開いている Claude Code は、一度閉じて開き直す。動いているプロセスは古い版のままだからだ。 Remote Control(claude remote-control)を常駐させている人は要注意だ。自分の Mac mini では、スマホから使うために claude remote-control を tmux の中で常駐させていた。これが10月16日に起動したまま、当時の 2.1.292 で動き続けていた。スマホから新しく作ったセッションも、全部この 2.1.292 で起動していた。claude update で 2.1.308 を入れても、常駐している親が古いままなら子も古い。 常駐している親ごと止めて、起動し直す。自分の環境(tmux セッション名 claude、LaunchAgent で起動)ならこうだ。動いているリモートのセッションは全部切れるので、作業が終わってから。 tmux kill-session -t '=claude' launchctl kickstart gui/$(id -u)/com.nekoai.remote-control # 自分で作った LaunchAgent の名前 ps -axo lstart,command | grep '[c]laude/versions/' # 新しい版で動いているか確認 Cursor を使っているなら CLI も。中に自前の Node を抱えている。 cursor-agent update # 公式インストーラーで入れた場合 brew upgrade --cask cursor-cli # Homebrew で入れた場合 自分の Mac では、cursor-agent update で 2026.10.22 版になって、中の Node が v26.8.1 から v26.12.1 に上がった。 デスクトップアプリも見ておく。10月23日、もう1台の Mac で Claude のデスクトップアプリを開いたら「Claude has an important security update」と出た。Mac mini のほうは、気づいたときには自動で入っていた。ChatGPT のデスクトップアプリにもアップデートが来ていた。中身が今回の件かは、どちらも公表されていない。Claude アプリの更新版(3.6556.1)の中の Node は v24.22.0 で、Node の修正版ではなかった。アプリのログを見ると、アプリに内蔵されている Claude Code が 2.1.304 から 2.1.308 に上がっていた。アプリの「新着情報」(10月22日付)には Node や Brotli の記載はなく、作業フォルダの権限や SSH まわりの変更・修正が並んでいた。「important」はそっちの話なのだと思う。それでも「important」と出ているなら入れる。 Mac の Claude アプリなら、画面上のメニューバーの「Claude」→「アップデートを確認…」→「再起動してアップデート」。アプリの中のアカウントメニューには無いので注意。 B:PC の Node まず、どこから入れた Node かを確認する。 which -a node ls -l "$(which node)" node -v ls -l の結果 入れ方 更新コマンド .../Cellar/node/... を指している Homebrew brew update && brew upgrade node ~/.nvm/... nvm 下を参照 fnm を含むパス fnm fnm install 24 && fnm default 24 ~/.volta/... Volta volta install node@24 mise を含むパス mise mise use -g node@24 /usr/local/bin/node で Cellar を指していない 公式インストーラー nodejs.org から最新版を入れ直す /usr/local/bin/node は、Intel Mac の Homebrew でもここに入る。ls -l で Cellar を指していたら Homebrew だ。Homebrew で node@22 や node@24 を入れているなら、brew upgrade node@24 のように指定する。 nvm は、入れるだけだと足りない。 nvm install 24 --reinstall-packages-from=current # npm -g で入れたものも引き継ぐ nvm alias default 24 nvm use 24 # 今のターミナルも切り替える プロジェクトの .nvmrc や、MCP の設定ファイルに ~/.nvm/versions/node/v22.x/... のような絶対パスを書いていたら、そっちも直す。古い版は nvm uninstall <古い版> で消せる。fnm・Volta・mise も、固定や絶対パスの事情は同じだ。 Windows はこう。 where.exe node の結果 入れ方 更新 C:\Program Files\nodejs\node.exe 公式インストーラー or winget winget list OpenJS で ID を確認して winget upgrade <ID>。無ければ nodejs.org から入れ直す ...\nvm... や nodejs へのリンク nvm-windows nvm install 24.22.1 → nvm use 24.22.1 .volta / fnm を含むパス Volta / fnm Mac と同じ winget の ID は、LTS 版なら OpenJS.NodeJS.LTS、最新版なら OpenJS.NodeJS。入れている方を上げる。 上げたら確認。 node -v v26.12.1 / v24.22.1 / v22.23.4 以降ならOK(v23・v25 は修正が出ないので D へ) ここからが本番。「まず一発で確認する」の最後の行をもう一度打つ。アップデートより前に起動したものは、全部古い Node で動いている。 Claude Code(ターミナル)→ /exit して起動し直す。裏の MCP サーバーも新しい Node で起動し直す Claude デスクトップアプリ・VS Code・Cursor → これらも MCP サーバーを起動している。ウィンドウを閉じるだけでなく、⌘Q(Windows はタスクトレイから終了)で完全に終了してから開き直す dev サーバー → Ctrl+C で止めて、もう一度起動 最後に、もう一度プロセスを確認する。自分で入れた Node のプロセスに、アップデートより前の起動時刻が残っていなければ、B は完了だ(アプリ同梱のものは残っていい)。 Node を同梱しているアプリ 自分で入れた Node とは別に、アプリが自前の Node を抱えていることがある。自分の Mac で、10月23日に見た版はこうだった。 アプリ 中の Node の版 Cursor の CLI(cursor-agent) v26.8.1 → 更新後 v26.12.1 Antigravity(Electron) v24.20.0 Grok Bot(SpaceXAI と Cursor が出しているアプリ。署名は Cursor の会社の Anysphere。Electron) v24.19.0 Claude デスクトップアプリ(Electron、更新版 3.6556.1) v24.22.0 ChatGPT デスクトップアプリ(中の補助ツール) v24.21.0(アプリ更新後も同じ) 版が修正前でも、そのアプリで攻撃が届くとは限らない。外から来た Brotli データをその Node で展開する場面があるかは、確認できていない。 Electron(VS Code、Cursor、Antigravity、Slack などの土台)は、10月23日時点の最新リリース(v45.0.0、10月21日)でも中の Node は 24.22.0 で、修正版ではなかった。 Cursor CLI 以外の同梱 Node は、こっちでは直せない。アップデートが来たら、すぐ入れる。それだけ。 Codex CLI は本体が Rust 製で、今回は関係ない。npm で入れた場合も、入口の小さなスクリプトが Node で動くだけだ。 C:公開しているアプリ 本番がどこで動いているかで変わる。 実行環境 自動で直る? やること Vercel 管理ランタイムに順次展開中。WAF ルールも入った Node 20 を使っているなら手動で移行。WAF は外向きの fetch() のレスポンスまでは守れない AWS Lambda 既定(Auto モード)なら自動で段階適用 Manual / Function update モードやコンテナイメージなら、再デプロイ・再ビルド Cloud Run(関数として、または --base-image 指定でソースからデプロイ) 自動ベースイメージ更新が有効なら反映される 設定を確認。今回の版の反映告知はまだ見つからなかった Cloud Run(自分の Dockerfile) 直らない 下の Docker と同じ。作り直して再デプロイ Firebase Functions 第2世代は Cloud Run の関数と同じ仕組みで、自動更新が既定で有効のはず コンソールでランタイムを確認し、心配なら再デプロイ Cloudflare Workers / Firebase Hosting(静的) 本番は Node で動いていない ビルドに使う Node だけ上げる Docker 公式イメージ node:22 / 24 / 26 は更新済み イメージを作り直して再デプロイ。node:20 は更新されない GitHub Actions setup-node の配布元には登録済み check-latest: true にするか、バージョンを固定で書く。ランナーのキャッシュに古い版が残っていることがある 自前サーバー 直らない B と同じ手順で更新して、プロセスを再起動 Docker は見落としやすい。FROM node:24 と書いてあっても、ビルドし直さないと古いイメージのままだ。 D:Node 20 以前を使っている Node 20 は2026年4月30日にサポートが終わっている。18・23・25 も終わっている。今回の修正は出ないし、今後も出ない。 Homebrew の node@20 は、10月28日に使えなくなる予定だ。 22 系か 24 系(どちらも長期サポート。26 系も10月28日に長期サポートに入る予定)に移す。破壊的変更があるので、テストを回しながら進める。今日すぐ終わる作業ではないので、それまでは「外部から来たデータを展開する処理」を止められないか考えておく。 確認コマンドと完了の判断は、教材にもまとめた。 nekoai-lab.github.io 時系列で見る 日時(日本時間) 出来事 2026年2月27日 Google の brotli リポジトリに、今回の修正のもとになるコミットが入る 10月14日 Node.js が「21日以降にセキュリティリリースを出す」と予告 10月21日 12時ごろ Claude Code 2.1.306 公開 10月21日 19時38分 Node.js v22.23.4 / v24.22.1 / v26.12.1 公開 10月22日 0時47分 最初の PoC リポジトリが GitHub に作られる 10月22日 4時27分 Google brotli が v1.2.1 / v1.1.1 / v1.0.10 を公開 10月22日 11時ごろ Bun が Brotli の修正をマージ 10月22日 13時半ごろ Claude Code 2.1.308 公開 10月22日 午後 Homebrew の node / node@24 / node@22 の修正版が配布される 10月22日 19時ごろ Bun v1.4.4 リリース 10月22日 23時ごろ CISA の評価は「悪用は確認されていない」 いくつか補足。 Brotli のバグは Node 固有じゃない。Google の brotli ライブラリ(1.0.3〜1.2.0)のバグで、CVE の採番も Google だ。CVSS 4.0 では 9.2(Critical)と付いていて、Node 側の評価は High。Brotli を使っている他のソフトも、同梱しているバージョンしだいで影響を受ける。 Node の発表では、Node への報告者として OpenAI の Codex Security チームの名前が載っている。上流の修正は2月に Google の開発者が入れていた。誰が最初に見つけたのかは、どの一次ソースにも書かれていない。 修正のもとになるコードは2月に brotli のリポジトリに入っていた。ただ、そのときはセキュリティ修正として出ていない。セキュリティ修正として世に出たのは10月21日の Node が最初で、brotli 自体のリリースはその翌日だった。 やった結果(実録) 対応は10月23日の朝の数時間。Mac 2台と本番4本。 対象 前 後 Mac mini の Node(Homebrew) v26.10.0 v26.12.1 Mac mini の Claude Code 2.1.306 2.1.308 Mac mini の Cursor CLI 同梱の Node v26.8.1 v26.12.1 Mac mini の Claude デスクトップアプリ 3.1623.4 3.6556.1(自動で更新。内蔵の Claude Code は 2.1.308) Mac mini の起動しっぱなしの Node 19個(最初のコマンドでは16個しか見えなかった) dev サーバー14個を止めた(親のプロセスも含めて15個を kill)。Claude アプリの MCP 1個はアプリの再起動で消えた。残りの MCP 4個は Remote Control を起動し直して消す MacBook Pro の Claude Code 2.1.283(9月26日の版) 2.1.308 MacBook Pro の Node v24.14.0(公式 pkg) Homebrew の v26.12.1 に切り替え MacBook Pro の起動しっぱなしの Node(全部 MCP サーバー) 11個(うち5個は9月19日から) 0個 本番(Cloud Run 4本) node:20 1本、node:22 3本 全部 22.23.4 で作り直してデプロイ。版チェック入り ChatGPT・Antigravity・Grok Bot の同梱 Node 修正前 アプリの更新待ち MacBook Pro はひどかった。 Claude Code は1か月前の版。Node で動いていた MCP サーバー11個のうち5個は、1か月以上前の9月19日から動き続けていた。その中には、ブラウザでページを取りに行く playwright-mcp もいた。今回のバグにいちばん当たりやすい種類だ。 Mac を閉じずにスリープで使い続けていると、こうなる。自動更新は「次に起動したとき」にしか効かない。 再発防止:「入れた」と「動いている」の差を仕組みで埋める 今回の穴は、どれも同じ形をしていた。 Node は上げた。でも動いているプロセスは古いまま Claude Code は上げた。でも常駐している親は古いまま Dockerfile は合っている。でも本番のイメージは作り直していない 自動更新はある。でも閉じないから効かない 人が気をつけて防ぐのは無理だと思った。なので仕組みにした。 1. Node の入れ方を全部の Mac で揃えた Mac mini は Homebrew、MacBook Pro は nodejs.org の公式インストーラーだった。入れ方が違うと、更新の手順も違う。だから後回しになる。 MacBook Pro を Homebrew に切り替えた。npm ls -g --depth=0 で、公式インストーラー側に自分で入れたツールが無いことを確かめてから。 以下は Apple Silicon の Mac(Homebrew が /opt/homebrew)で、公式インストーラーから乗り換える場合だ。Intel Mac は Homebrew 自体が /usr/local に入るので、この rm を打つと Homebrew の Node まで消える。先に brew --prefix が /opt/homebrew と出ること、ls -l /usr/local/bin/node が Cellar を指していないことを確かめる。 brew install node 公式インストーラーの Node を消す(npm -g で入れたものがあれば、先に控えておく) sudo rm -rf /usr/local/bin/node /usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack \ /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/corepack \ /usr/local/include/node /usr/local/share/doc/node /usr/local/share/man/man1/node.1 ターミナルを開き直して which -a node # /opt/homebrew/bin/node だけになれば OK これで、どの Mac も brew upgrade node の1行で済む。 2. 本番の Dockerfile に「古い Node なら作らない」を入れた FROM node:22-alpine と書いていても、作り直さなければ古いまま。逆に、キャッシュや取り違えで古いイメージが混ざることもある。 なので、本番のイメージを作るときに Node の版を確かめて、修正前なら止めるようにした。実行ステージの FROM の直後に1行足すだけだ。 FROM node:22-alpine AS runner Node.js 2026-10-21 のセキュリティ修正より前の版では作らない RUN node -e "const m={22:[23,4],24:[22,1],26:[12,1]};const [a,b,c]=process.versions.node.split('.').map(Number);const min=m[a];const ok=a>26||(min&&(b>min[0]||(b===min[0]&&c>=min[1])));if(!ok){console.error('Node '+process.version+' は修正より前か、サポート外');process.exit(1)}" 22.23.4・24.22.1・26.12.1 より前と、20 系などの修正が出ない系列は、ここでビルドが止まる。4本とも入れて、作り直し、デプロイまで通した。 このチェックが効くのは、作り直すときだけだ。作り直していない本番が古いままなのは、これでは分からない。 次は Dependabot に FROM node:.. を見張らせて、新しいイメージが出たら PR が自動で立つようにする(これから)。 3. 週1回、まとめて上げて「古いまま動いているもの」を探す 2つのスクリプトを作った。 weekly-update:Homebrew で入れたもの全部(Node 以外も)、Claude Code、Cursor CLI を上げる。macOS のアップデートが来ていれば知らせる(入れはしない。再起動が要るから) stale-procs:更新したのに、古い版のまま動いているプロセスを探す stale-procs の考え方は単純だ。プロセスの実行ファイルを lsof で調べて、いまの node の実体と違えば古い。Homebrew のものなら、Cellar/<名前>/<版> が opt/<名前> の指している版と違えば古い。Node や Python に限らず、Homebrew で入れたもの全部に使える。 Node だけなら、これで同じことが分かる。 cur=$(realpath "$(command -v node)") for pid in $(pgrep -f 'node|next-server'); do exe=$(lsof -a -p "$pid" -d txt -Fn 2>/dev/null | sed -n 's/^n//p' | head -1) case "$exe" in /Cellar/node/bin/node|/usr/local/bin/node|/.nvm//bin/node) [ "$(realpath "$exe" 2>/dev/null || echo "$exe")" != "$cur" ] && echo "$pid 古い Node: $exe" ;; esac done next-server のように名前を変えているプロセスも、実行ファイルで見るので拾える。アプリ同梱の Node は、自分では直せないので数えない。 ただし、これで分かるのは Homebrew と nvm で入れた Node だけだ。公式インストーラーの Node は同じ場所に上書きされるので、この比べ方では新旧を見分けられない。fnm・Volta・mise も、この短い版では対象にしていない。その場合は、プロセスの起動時刻が Node を上げた時刻より前かどうかで判断する。 結果はスマホに通知する。ただ、普段から通知は短すぎて読めていなかった。「更新されたな」と分かるだけで、何をすればいいかは分からない。なので、通知だけで次の行動が決まる形にした。 古い版のまま動いているもの 9 件 ・Remote Control・アプリから起動したセッション 4件(claude 2.1.292)  → 親(Remote Control)を作り直すか、そのセッションを閉じる ・MCP サーバー 4件(mcp-pdf-server/node 26.10.0_1)  → 起動したアプリやセッションを開き直す ・Remote Control の親 1件(claude 2.1.292)  → tmux のセッションを作り直す 4. 通知に「プロセスの引数」を出してはいけなかった このスクリプトは Claude Code で書いて、レビューは別の AI(Codex)にさせている。そのレビューで2回止められた。 1回目は、通知にプロセスのコマンドをそのまま載せていた。node app.js --password xxx のような起動のしかたをしていたら、パスワードがログとスマホの通知に流れる。 2回目は、それを直したつもりで「引数のうち、ハイフンで始まらない最初のもの」をスクリプト名として出していた。node -e 'key="..."' のように中身を直接渡すと、そのコードが名前として出る。 最後は「実在するファイルのときだけファイル名を出す。それ以外は node」にして通った。 プロセスの一覧を通知やログに出すスクリプトは、引数を出した時点で秘密が漏れる道になる。自分のレビューだけだったら、2回とも素通りしていたと思う。 5. 片付けをどこまで自動にするか(これから) 通知が来ても、止めるのが人の手作業なら、結局また溜まる。かといって、全部を自動で止めると作業中の dev サーバーやセッションが切れる。なので3段階に分けた。 段階 対象 誰が 全自動 起動した元がもういなくて取り残された古いプロセス。使われていないときの Remote Control の親 週1回の自動実行。止めたものは通知で報告 ひと言で 開いているセッションの MCP、dev サーバー 通知を見て、AI に「古いの片付けて」と頼む。一覧を見せて確認してから止める 人が決める デスクトップアプリ、macOS のアップデート 自分。通知に「何をすればいいか」が書いてある MacBook Pro の11個(うち5個は9月19日から)は、たぶん「全自動」で消える種類だった。これを作れば、今回の後片付けの大半は、寝ている間に終わる。 この部分は、設計まで書いて Codex に引き継いだ。 32リポジトリぶんの依存を、そのあとどう見回るようにしたかは、続きに書いた。 zenn.dev まとめ AI ツールを使っている人:claude update、Cursor CLI、デスクトップアプリを最新に。開き直す Node が入っている人:Node を上げて、動いているものを全部再起動。アプリ同梱の Node はアプリの更新で 公開している人:本番の実行環境を確認して、再デプロイ。Docker は作り直す Node 20 以前の人:今回は直らない。乗り換える 全員:「入れた」と「動いている」の差は、気合いでは埋まらない。週1回の更新と、古いまま動いているものの検出を仕組みにしておく アップデートは、インストールした時点では終わらない。 古いプロセスを止めた時点で、やっと終わる。 参考 Node.js — October 2026 Security Releases google/brotli v1.2.1 CVE-2026-111497(CVE.org API) oven-sh/bun #46352 — Brotli の脆弱性 Bun v1.4.4 ikow/brotli2crash — PoC と解析(実質 DoS とする例) hypnguyen1209/CVE-2026-111497 — Express を使った PoC Claude Code — セットアップ(ドキュメント) anthropics/claude-code — Releases expressjs/body-parser — read.js Electron v45.0.0 SpaceXAI and Cursor Launch Grok Bot Agent App in Beta(AI Weekly) Vercel — Node.js security advisory AWS Lambda — ランタイムの更新 Cloud Run — 自動ベースイメージ更新 actions/setup-node — check-latest Node.js リリーススケジュール

    4 likes
  6. So true (CVE-2026-113872)! I am working now to publish this CVE and others but the advisory is already published github.com/expressjs/multer/security/advisories/G…

  7. Node.js October 2026 Security Release: Finding Every Node You Actually Run --- title: Node.js October 2026 Security Release: Finding Every Node You Actually Run published: true date: 2026-10-22 09:00:00 UTC tags: Linux,Nodejs,Security,pm2 canonical_url: devops-daily.com --- On 21 October 2026 the Node.js project released 22.23.4, 24.22.1 and 26.12.1. The headline fix is CVE-2026-111497, an out-of-bounds write in the Brotli decoder that the advisory says "permits remote code execution". The same release fixes two TLS certificate hostname-check bypasses, a DNS bug that truncates names at a NUL character, and updates the bundled CA store. End-of-life lines get no fix. Patching Node is a one-line version bump. Finding every Node your servers actually run is the work. When we patched a small production box on release day, six Node apps under pm2 installed with nvm, installing the new version was the easy part. Then we found pm2 entries with the old binary written into them, a systemd unit with the old PATH hard-coded, child processes we had not thought about, and cron jobs on two other boxes that pointed at the old install by absolute path. None of them moved when the default version changed. This post covers what to do now, who is exposed (including services that call fetch() on a URL a user controls), the upgrade steps for nvm, pm2, systemd and containers with the traps we hit, how to verify with /proc, and a read-only audit script with its real output from one of our machines. What to do now 1. Upgrade every Node to 22.23.4, 24.22.1 or 26.12.1 or later. Node 20, 25 and older are end-of-life and get no fixed release. Move them to 22, 24 or 26. 2. List every place Node runs or will start : running processes, pm2 entries (live and saved), the pm2 daemon, systemd units, crontabs and helper scripts, and containers. Changing the nvm default moves none of them. 3. Move each one with the steps below, then check the real binary of every process in /proc. Process names do not help: on our build machine pgrep node found 0 of 5 Node processes. 4. Rebuild images with --pull and redeploy. The official node:22, node:24 and node:26 tags have the fix. node:20, node:25 and node:18 will not get it. 5. If a service cannot be upgraded today and accepts Brotli data from strangers, close that path in front of it (details below). For outbound fetch() the fix is the upgrade; until then, turning off features that fetch user-supplied URLs is the way to close that path. 6. Run the audit script on every host and keep the output. Prerequisites - Shell access to the hosts that run Node, as the user that owns the Node processes. Root is not needed. - Python 3.8 or newer for the audit script. It uses only the standard library and only reads. - The docker or podman CLI on hosts that run Node in containers. What was fixed The advisory lists ten vulnerabilities. The four rated high: | CVE | What the advisory calls it | Lines | | --- | --- | --- | | CVE-2026-111497 | Brotli decoder out-of-bounds write permits remote code execution | 22, 24, 26 | | CVE-2026-105483 | TLS certificate hostname bypass when the IP is written in fullwidth digits | 24, 26 | | CVE-2026-106069 | TLS certificate hostname bypass when the host loosely resolves to an IP address | 22, 24, 26 | | CVE-2026-105482 | dns.resolve truncates names containing a NUL character | 22, 24, 26 | The medium ones are a keep-alive socket that processes a response written before the request was sent (CVE-2026-105481), DNS SANs that can authenticate IP endpoints written in a non-canonical IPv4 form (CVE-2026-106072), and crypto.randomUUID() and randomInt() caches saved into startup snapshots and single executable applications, so that every process repeats the same values (CVE-2026-106074). The low ones are two Permission Model bypasses and a ZIP header issue on 26.x. The release also updates the bundled root certificates to NSS 3.131.1 and makes Node reject certificate chains that contain blocked public keys. The advisory lists this as the "ca-cert (eMudhra Intermediate CA Compromise)" update. The bundled undici, c-ares and OpenSSL (22 and 24) are updated too. Two things the advisory does not give you yet. There is no technical detail on any of the bugs, and on the morning of 22 October all ten CVE IDs were still in the RESERVED state at CVE.org, with no NVD record. So there is no CVSS score; "high" is the Node.js project's rating. The timeline in UTC, from the release metadata: - 21 Oct, 10:32 to 10:36 : the fixed binaries on nodejs.org/dist are dated in this window. The GitHub releases were published at 10:38. - 10:48 : the update to the official Docker images was committed. - about 11:35 : the Debian-based official images were built. Docker Hub shows the amd64 and arm64 images as pushed at about 14:40. - 19:27 : the Brotli project released 1.2.1, 1.1.1 and 1.0.10, each with "CVE-2026-111497: out-of-bounds write permits remote code execution" in the notes. So the bug is in the Brotli decoder itself, and Node shipped the upstream fix as a cherry-picked commit. - 22:58 to 00:08 : the Alpine-based official images were built, about 12 to 13.5 hours after the release. Who is exposed Brotli: any Node process that decodes Brotli someone else chose The decoder is compiled into the node binary and sits behind node:zlib. Every way into it counts, and three are common: 1. Your code decompresses Brotli directly : zlib.brotliDecompress(), brotliDecompressSync() or createBrotliDecompress() on uploads, archives, cache entries or messages. 2. Your service fetches URLs. fetch() in Node is the bundled undici, and it decodes responses that say Content-Encoding: br with zlib.createBrotliDecompress. It goes by the response's header, whatever the request asked for, so setting Accept-Encoding: identity does not keep Brotli away from a hostile server. axios in Node does the same unless you pass decompress: false. Link previews, webhook testers, feed readers, "import from URL" features and AI agents that browse are all services that fetch a URL a user chose. 3. Your service accepts compressed request bodies. body-parser 2 decompresses br request bodies that it parses, and its inflate option defaults to true. Express 5 ships body-parser 2, so express.json() on Express 5 decodes Brotli bodies sent with a JSON content type, from any client. Express 4 uses body-parser 1.x, which handles only gzip and deflate. Reaching the decoder is not the same as being exploitable, and the advisory does not say what input triggers the bug. These are the paths to upgrade or close first. Because the decoder is in the binary, no npm update fixes it. Upgrading the undici package in your package.json, for example, still decodes through the old Node's zlib. If you cannot upgrade a service today, the interim options close only the paths you control: // Express 5: refuse compressed request bodies (415) until Node is upgraded. // This also refuses gzip and deflate request bodies. app.use(express.json({ inflate: false })); # In front of any Node service: refuse Brotli request bodies. if ($http_content_encoding ~* "br") { return 415; } Distribution builds are different. Debian's nodejs package (we checked the trixie build of 20.19.2) is built with --shared-brotli against the system libbrotli, so on Debian the Brotli fix comes from the distribution's brotli package, not from the Node version number. At the time we checked, neither Debian's security tracker nor Ubuntu's notices listed CVE-2026-111497 yet. The audit script reports such builds as unknown rather than guessing. TLS hostname checks and DNS When Node connects over TLS it checks the server's certificate against the host you asked for. Both high-severity TLS bugs are in that check when the host is, or loosely resolves to, an IP address written in a non-standard form: fullwidth digits (24 and 26 only) or a form that "loosely resolves to an IP address" (all lines). The fix makes Node reject non-canonical IP addresses. The advisory gives no more detail than that. Our reading is that the services to look at first are the ones where someone else chooses the host string: webhook senders, URL fetchers and integrations that connect to customer endpoints. The DNS bug truncated names at a NUL byte in dns.resolve*(), and the fix rejects such input. Code that resolves names it got from users is where this matters. End-of-life lines "End-of-Life versions are always affected when a security release occurs," says the advisory. Node 20 reached end of life on 30 April 2026 and Node 25 on 1 June 2026. Neither gets a fixed release, and the official node:20 and node:25 image tags stay on 20.20.2 and 25.9.0. Why changing the default moves nothing nvm installs the new version next to the old one, which is what you want: the old one stays for rollback. nvm alias default 24.22.1 then changes what new shells get. Nothing that is already running reads that alias, and nothing that stored a path to the old install reads it either. 1. nvm install 24.22.1 next to the old version 2. nvm alias default new shells get it 3. What runs the new Node? check /proc, not config Outcomes: - New shells, and pm2 apps restarted with --update-env from one - pm2 daemon, cluster workers, absolute interpreters, systemd unit, cron, images: still old On the production box, each of these needed its own step: - pm2 entries with an absolute interpreter. Several entries had the old Node binary written into them as the interpreter, and the ones started as npm or npx had their script path under the old version's bin directory. Changing the default did not move them. They had to be deleted and recreated with the new interpreter. - pm2 entries with a bare node interpreter took the new binary only after pm2 restart <name> --update-env from a shell where the new Node came first in PATH. - The pm2 systemd unit that pm2 startup wrote had the old version's PATH and the old pm2 path hard-coded. After editing it, pm2 update moved the pm2 daemon itself, which restarts every app at once. The sites on that box were down for under a minute while the Next.js apps started. - Child processes. Checking /proc/<pid>/exe for every process turned up chains such as npx to tsx to node, and npm start to next-server. Each process in a chain has its own binary. - Cron jobs and helper scripts on two other boxes had absolute paths to the old Node. What pm2 really does on an upgrade To check each of those behaviors on its own, lab/pm2-pin-lab.sh in the repo starts four throwaway apps in a separate PM2_HOME from a shell where Node v24.14.0 comes first in PATH: bare (pm2 start server.js), via-npm (pm2 start npm -- start, where the start script is node server.js), abs (--interpreter set to the old binary) and clustered (-i 1, cluster mode). Then it puts v24.22.1 first in PATH, as a changed nvm default does, and records the real binary of every process after each step. A plain pm2 restart left all four on v24.14.0. This is the state after pm2 restart <name> --update-env for each app: pm2-pin-lab.sh, step B2 # excerpt of results/pm2-lab-20261022T063002Z.txt (2026-10-22T06:30:02Z, aarch64, pm2 6.0.14) # old: v24.14.0, new: v24.22.1 first in PATH; a plain restart left all four apps on v24.14.0 $ lab/pm2-pin-lab.sh <v24.14.0 bin> <v24.22.1 bin> $ pm2 restart bare --update-env $ pm2 restart via-npm --update-env $ pm2 restart abs --update-env $ pm2 restart clustered --update-env == B2. pm2 restart --update-env from the new shell daemon pid 1937570 node v24.14.0 <v24.14.0>/bin/node bare interpreter=node script=<app>/server.js saved PATH finds node in <v24.22.1>/bin pid 1938284 node v24.22.1 <v24.22.1>/bin/node server.js via-npm interpreter=<v24.14.0>/bin/node script=<v24.14.0>/bin/npm saved PATH finds node in <v24.22.1>/bin pid 1938375 node v24.14.0 <v24.14.0>/bin/node npm start pid 1938462 dash (not node) /usr/bin/dash sh -c node server.js pid 1938468 node v24.22.1 <v24.22.1>/bin/node node server.js abs interpreter=<v24.14.0>/bin/node script=<app>/server.js saved PATH finds node in <v24.22.1>/bin pid 1938455 node v24.14.0 <v24.14.0>/bin/node server.js clustered interpreter=node script=<app>/server.js (cluster mode) saved PATH finds node in <v24.22.1>/bin pid 1938518 node v24.14.0 <v24.14.0>/bin/node server.js bare moved. via-npm is split: npm itself still runs on the old Node because pm2 stored the old binary as its interpreter, while its child node server.js got the new one from PATH. abs did not move. clustered did not move either, even though its saved PATH now finds v24.22.1: in cluster mode the daemon forks the workers with its own Node binary, so they move only when the daemon moves. All four steps, from results/pm2-lab-20261022T063002Z.txt: | App | After restart | After restart --update-env | After delete and recreate | After pm2 update | | --- | --- | --- | --- | --- | | bare (bare node) | old | new | not recreated: new | new | | via-npm (npm path stored) | old | npm old, app new | new | new | | abs (absolute interpreter) | old | old | new | new | | clustered (cluster mode) | old | old | not recreated: old | new | | pm2 daemon | old | old | old | new | pm2 update restarted every app in one command: all four PIDs changed. On the production box that was the step where the sites went down. In the lab the command took 2.3 seconds; that measures the command, not how long the apps were unavailable, and real apps take as long as they take to start. Upgrade steps: nvm, pm2 and systemd Run these as the user that owns the pm2 daemon. # 1. Install next to the old version and bring global packages (pm2) along. nvm install 24.22.1 --reinstall-packages-from=24.14.0 nvm alias default 24.22.1 # 2. Work from a shell where the new Node comes first in PATH. nvm use 24.22.1 command -v node && node --version # 3. See what each pm2 entry is pinned to. pm2 jlist | python3 -c 'import json, sys for a in json.load(sys.stdin): e = a["pm2_env"] print(a["name"], e.get("exec_mode"), e.get("exec_interpreter"), e.get("pm_exec_path"), sep=" ")' Then move each entry according to what step 3 shows: # Fork mode, interpreter "node": pick up the new PATH. pm2 restart api --update-env # Interpreter is an absolute path to the old node, or the script is # .../v24.14.0/bin/npm or npx: delete and start it again from this shell # (or from your ecosystem file with the same name and arguments). pm2 delete web pm2 start npm --name web -- start # Save the list that a reboot brings back. pm2 save # Regenerate the startup unit: the old one hard-codes the old PATH and pm2 path. pm2 unstartup # prints a sudo command; run it pm2 startup # prints a sudo command; run it # Move the daemon, and every cluster-mode app with it. # This restarts ALL apps at once: do it in a quiet window. pm2 update The pm2 docs ask for the unstartup and startup pair after every Node upgrade. The new unit is pinned again, this time to the new version. That is fine, as long as something checks it at the next upgrade. Two details are easy to miss. pm2 save writes the saved list, including each entry's environment, so if you save before --update-env the reboot copy keeps the old PATH. And the daemon only reads the systemd unit when systemd starts it. If someone started pm2 by hand, the fixed unit applies at the next boot, not now. Cron jobs and helper scripts Cron runs with a minimal PATH, so jobs that need Node usually name it by absolute path, and that path names one install. The audit script below reads only your own crontab -l, so search the system crontabs and your scripts on every host too: # Paths inside a versioned nvm, fnm, n, volta or asdf install sudo grep -rnE '/(\.nvm/versions/node|node-versions|n/versions/node|\.volta/tools/image/node|installs/nodejs)/v?[0-9]+\.[0-9]+\.[0-9]+' \ /etc/crontab /etc/cron.d /var/spool/cron /etc/systemd/system /usr/local/bin ~/bin 2>/dev/null Update the path in each match. A job that points at a version you later uninstall stops running, which at least fails loudly; one that points at the old version you kept for rollback keeps running vulnerable and says nothing. Containers A container keeps the Node it was built with. Rebuild with --pull so the builder fetches the new base image instead of a cached one, deploy by the new tag or digest, and check what is running: docker build --pull -t registry.example.com/app:2026-10-22 . docker ps --format '{{.ID}} {{.Names}}' | while read -r id name; do printf '%-30s %s\n' "$name" "$(docker exec -e NODE_OPTIONS= "$id" node --version 2>&1)" done node --version inside a container reports the node on the container's PATH. If an image carries more than one Node, docker exec <id> readlink /proc/1/exe shows which binary the container's main process runs (when the image has readlink). The repo's registry_scan.py read the Node version of 24 official node tags for amd64 and arm64 from registry metadata only (manifest and image config, no layers) at 06:23 UTC on 22 October. Every 22, 24 and 26 tag we sampled, and the latest, lts and alpine aliases, shipped the fixed version. How long they took: Official node images: hours from the release to build and to push | | Value | Series | | --- | --- | --- | | Debian-based amd64: built | 1 h | built | | Debian-based amd64: pushed | 4.1 h | pushed | | Debian-based arm64: built | 1 h | built | | Debian-based arm64: pushed | 4.1 h | pushed | | Alpine amd64: built | 12.4 h | built | | Alpine amd64: pushed | 13.1 h | pushed | | Alpine arm64: built | 13.5 h | built | | Alpine arm64: pushed | 16.1 h | pushed | _Reference: fixed binaries on nodejs.org/dist by 10:36 UTC on 21 Oct 2026. Built: the image config's created time. Pushed: Docker Hub's last_pushed time for that platform image, which is metadata, not proof of when a tag first served it. Latest value in each group of tags. Source: results/registry-20261022T062338Z-summary.txt._ If you rebuilt an Alpine-based image on the afternoon of 21 October, check it again: by this metadata the fixed Alpine images were built 12 to 13.5 hours after the release. The end-of-life tags will stay where they are: node:18 (18.20.8, built March 2025), node:20 (20.20.2, built April 2026) and node:25 (25.9.0, built April and May 2026). Verify with /proc, not with names pm2 renames its daemon to PM2 v6.0.14: God Daemon and Next.js renames its server to next-server, and on our build machine the plain node processes showed up as MainThread. Those names are what pgrep matches and what top shows. On that machine pgrep -c node returned 0 while five processes ran a node binary (results/pgrep-vs-proc-20261022T063334Z.txt). The kernel's link to the executable does not lie: # Every process whose binary is node, with the binary it really runs (root sees all users) for d in /proc/[0-9]*; do exe=$(readlink "$d/exe" 2>/dev/null) || continue case "${exe% (deleted)}" in */node|*/nodejs) ;; *) continue ;; esac printf '%-8s %-70s %s\n' "${d#/proc/}" "$exe" "$(tr '\0' ' ' < "$d/cmdline" | cut -c1-50)" done # The version of one of them, read from the binary without running it. # Official builds compile in their headers URL. Do not run another user's # binary with --version as root: that runs their code as root. sudo grep -a -o -m1 -E 'node-v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?-headers\.tar\.gz' /proc/12345/exe # node-v24.14.0-headers.tar.gz # Its children, which have their own binaries ps -o pid,args --forest -g "$(ps -o sid= -p 12345 | tr -d ' ')" An exe that ends in (deleted) means the file was replaced on disk after the process started, so the process still runs the old code until it restarts. For more on that pattern, and on proving a runtime is patched when the version string can mislead, see PHP2Shell: How to Check That Every PHP You Run Is Actually Patched. Run the audit script The-DevOps-Daily/node-runtime-audit on GitHub node_runtime_audit.py is one file of standard-library Python. Its sources are /proc, the output of pm2 jlist and crontab -l, systemd unit files, and node --version inside running containers; besides those it only runs systemctl show for pm2 units and docker or podman ps and inspect, and looks at file metadata. For every running process it reads the real binary through /proc/<pid>/exe and the version compiled into that file, which works for deleted binaries and for processes inside containers. By default it never runs a binary it finds on the host, and it does not open the Node binaries that pm2 entries, crontabs and units name: their version comes from a running process that uses the same file, or from the path as a hint, where a path that names an unfixed version counts as vulnerable and one that names a fixed version stays unknown. Files are pinned with O_PATH and checked before they are read, so a planted FIFO or device cannot stall it. Command lines and container names are printed with control characters escaped. Run it as the user that owns the Node processes; root is not needed. This is the real output on the Raspberry Pi we build on, run as the user that owns the Node processes, with --redact, which hides user names and app names. By the time of this run, the fnm default, the crontab and the pm2 systemd unit had already been moved to v24.22.1: node_runtime_audit.py on the Pi # 2026-10-22T07:10:33Z Raspberry Pi (Debian 12, aarch64), Node via fnm, pm2 6.0.14, run as the user that owns the Node processes $ python3 node_runtime_audit.py --redact node-runtime-audit: Node.js security release of 2026-10-21, fixed releases v22.23.4, v24.22.1, v26.12.1 lines without a fix (end-of-life) count as vulnerable note: covers the processes, crontab and pm2 of USER; run it as each user that owns Node processes Running Node processes (real binary from /proc/<pid>/exe) [vulnerable] pid 1883303 v24.14.0 USER /home/USER/.local/share/fnm/node-versions/v24.14.0/installation/bin/node cmd: PM2 v6.0.14: God Daemon (/home/USER/.pm2) parents: systemd(1) why: v24.14.0 < v24.22.1 [vulnerable] pid 1883371 v24.14.0 USER /home/USER/.local/share/fnm/node-versions/v24.14.0/installation/bin/node cmd: node /home/USER/projects/app-1/node_modules/.bin/next dev parents: bun(1883370) <- node(1883303) <- systemd(1) why: v24.14.0 < v24.22.1 [vulnerable] pid 1883390 v24.14.0 USER /home/USER/.local/share/fnm/node-versions/v24.14.0/installation/bin/node cmd: next-server (v16.2.6) parents: node(1883371) <- bun(1883370) <- node(1883303) <- systemd(1) why: v24.14.0 < v24.22.1 [vulnerable] pid 1884634 v24.14.0 USER /home/USER/.local/share/fnm/node-versions/v24.14.0/installation/bin/node cmd: node /home/USER/projects/app-1/.next/dev/build/postcss.js 38963 parents: node(1883390) <- node(1883371) <- bun(1883370) <- node(1883303) why: v24.14.0 < v24.22.1 183 processes of other users not inspected: run the script as their owner pm2 live list (pm2 jlist, PM2_HOME=/home/USER/.pm2) [unknown] pm2 CLI /home/USER/.bun/bin/pm2 is writable by group or others (mode 777): not run, live list skipped [vulnerable] daemon pid 1883303 v24.14.0 /home/USER/.local/share/fnm/node-versions/v24.14.0/installation/bin/node Installed Node versions (version managers) fnm in /home/USER/.local/share/fnm installed: v24.14.0 (running), v24.14.1 (from path), v24.22.1 (from path) default alias -> v24.22.1 (from path) `node` on this shell's PATH -> v24.22.1 (from path) /home/USER/.local/share/fnm/node-versions/v24.22.1/installation/bin/node for information only: these do not change the exit code Crontab: absolute Node paths [unknown] crontab -l (USER):33 /home/USER/.local/share/fnm/node-versions/v24.22.1/installation/bin/node -> v24.22.1 (from path) (pinned to one install) why: from path: v24.22.1 (a hint, not proof: binaries outside /proc are not read) [unknown] crontab -l (USER):53 /home/USER/.local/share/fnm/node-versions/v24.22.1/installation/bin/node -> v24.22.1 (from path) (pinned to one install) why: from path: v24.22.1 (a hint, not proof: binaries outside /proc are not read) systemd units: absolute Node paths [unknown] /etc/systemd/system/pm2-USER.service:12 /home/USER/.local/share/fnm/node-versions/v24.22.1/installation/bin -> v24.22.1 (from path) (pinned to one install) why: from path: v24.22.1 (a hint, not proof: binaries outside /proc are not read) unit: pm2-USER.service ActiveState=inactive MainPID=0 note: the unit is not what runs pm2 now; its PATH applies at the next boot Containers no docker or podman CLI found result: VULNERABLE: something runs or will start an unfixed Node (exit 1) $ echo $? 1 The fnm default, the shell's node, both cron jobs and the systemd unit point at the v24.22.1 install. The script reports the cron and unit paths as unknown rather than patched, because no running process uses that binary and it does not open it. The four processes that run are v24.14.0. The pm2 daemon is not running under the systemd unit (ActiveState=inactive), so the unit we had fixed is not what runs it now. The one pm2 app is started by bun, and the Node processes under it run the app's node_modules/.bin/next script. A separate check (scripts/pm2-path-evidence.py, results/pm2-path-20261022T070504Z.txt) shows that the process the pm2 daemon started and the saved pm2 list both have the v24.14.0 install first on PATH, which is where those Node processes come from. If the app is resurrected from that saved list after a reboot, it gets the same PATH; we did not reboot to confirm. The fix here is pm2 restart <app> --update-env from a fixed shell, then pm2 save, then moving the daemon. The run found one more thing: the pm2 CLI on that machine is world-writable (mode 777). The script will not run a CLI that someone else could have changed, so it skipped the live pm2 list and reported it as unknown. Check the permissions of your Node installs and global CLIs while you are in there. The exit code is 1 when anything runs or will start a vulnerable Node, otherwise 2 when anything could not be checked, and 0 when it found nothing vulnerable or unknown within what it covers, including no Node at all. Unknown never counts as patched: a pre-release build, a distribution build linked to the system Brotli, a binary without an embedded version, a unit it could not read, a failed pm2 jlist, crontab -l or docker ps, a config path that only names a fixed version, or a cron path to a Node that no longer exists all give 2. It only sees what the user running it can see, so run it as each user that owns Node processes. To run it across hosts without copying it first: for h in deploy@web1 deploy@web2 deploy@web3; do ssh "$h" 'python3 - --json' < node_runtime_audit.py > "node-${h#*@}.json" echo "$h exit $?" done The tests build fake hosts with the cases a version check gets wrong: a fake node that would leave a marker file if the script ever ran it, a binary replaced while it runs, a container binary, a Debian-style build, a pm2 entry whose config is fixed but whose process was never restarted, npm pinned to the old install while its child gets the new one, a cluster-mode app, an old binary in a directory named after a fixed version, end-of-life and pre-release versions, a cron job on a removed version, a shell wrapper that mentions a fixed version, units owned by other users, and FIFOs, devices, hard links and symlinks planted where a file is expected. One test starts a real process, deletes its binary and checks that the script still reads the running file. Rollback Keep the old version installed until every check passes. Rolling back is the same work in reverse: nvm alias default 24.14.0, then the same --update-env, delete-and-recreate, pm2 save, startup unit and pm2 update steps from a shell where the old version comes first. For containers, redeploy the previous image digest. A rollback puts the vulnerable version back, so treat it as a few hours to fix a regression, not as a resting state. Once the audit shows nothing on the old version, nvm uninstall 24.14.0. Anything still pinned to it will then fail to start instead of quietly running the old code, and the audit script reports those paths as missing. What we could not check - The bugs themselves. The advisory gives no technical detail, and we did not test whether any code path reaches the Brotli flaw. The exposure section is based on which code calls the decoder. - Scores. The CVE records were still reserved when we checked, so there is no CVSS score. - Distribution packages. Debian and Ubuntu had not published anything for CVE-2026-111497 when we checked. Check your distribution's nodejs and brotli advisories. - Containers on a real host. The Pi has no Docker, so the container checks are covered by the test fixtures only. The registry scan read real image metadata. - The downtime number."Under a minute" comes from the production upgrade. The lab's pm2 update took 2.3 seconds because its apps do nothing. - Bare node in cron jobs and units. The script checks absolute paths and PATH= lines; it does not resolve a bare node through the job's PATH. It reads only your own crontab -l, not the system crontabs or the saved pm2 list, and it does not follow EnvironmentFile=. - Other platforms. The registry scan covers linux/amd64 and linux/arm64 only. The script needs Linux and /proc, and does not cover Kubernetes, stopped containers, or Node binaries renamed to something other than node (single executable applications, Electron). Checklist - <input disabled type="checkbox"> Every host: Node 22.23.4, 24.22.1 or 26.12.1 or later installed; Node 20, 25 and older replaced - <input disabled type="checkbox"> Every pm2 entry: absolute interpreters and npm or npx script paths recreated, bare node entries restarted with --update-env, then pm2 save - <input disabled type="checkbox"> pm2 startup unit regenerated (pm2 unstartup, pm2 startup) and the daemon moved with pm2 update in a quiet window - <input disabled type="checkbox"> Cluster-mode apps checked after the daemon moved - <input disabled type="checkbox"> Crontabs, systemd units and helper scripts searched for absolute paths to the old install - <input disabled type="checkbox"> Images rebuilt with --pull (Alpine-based ones checked after the late rebuild) and redeployed by new tag or digest - <input disabled type="checkbox"> Services that fetch user-supplied URLs or accept compressed bodies upgraded first; interim measures where they cannot be - <input disabled type="checkbox"> Every Node process checked by /proc/<pid>/exe, not by name, including child processes - <input disabled type="checkbox"> Node installs and global CLIs (pm2, npm) writable only by their owner - <input disabled type="checkbox"> Audit script output saved per host, and the old version uninstalled once it is clean Every fact in this post is listed with its source in the repo's SOURCES.md. _Originally published at devops-daily.com._

    5 reactions
  8. @avinxshKD there are so many. its crazy: github.com/expressjs/express/pulls

    14 views
  9. YouTube · · YouTube description · Chart Design · expressjs/express

    週刊プログラミングニュース|Deno・Python・OSSと開発ツール【2026-W42/10月9〜15日】 週刊プログラミングニュース|2026年10月9日〜15日 | 2026-W42 最初にDeno、Python、Bun、GitHub Actions、.NET、シークレット保護。その後、OSS・フレームワーク、ライブラリー・データベース、IDE・開発プラットフォームを章ごとに整理します。 対象期間:10月9日00:00〜15日23:59 JST。時刻を確認できた発表はJSTへ換算し、日付のみの資料は公式表記日を採用しています。境界時刻が未確認の候補は確定分と区別しました。 正式版、プレビュー、今後の予定を区別しています。Dockerは修正済み問題の開示、Nodeの10月21日予定分は未提供の修正予告です。 映像は公式情報に基づく解説図で、実機デモではありません。性能値は発表元の測定で、独自の実測ではありません。 日本語音声・日英字幕。 チャプター 00:00 週刊プログラミングニュース 00:28 DenoチームがCloudflareへ 01:50 Python 3.15が正式公開 03:18 Bun 1.4.3に型チェッカー 04:30 Actionsにワークフローロック 05:38 .NETは安定版更新とRC2 07:00 GitHubのシークレット検出拡大 07:59 Next.jsのセキュリティ更新 09:28 Express・Fastify・Micronaut 10:57 フロントエンドの安定版とRC 12:16 Astro・Rolldown・Laravel 13:21 Hydrogenがライブラリーへ 14:14 Pydantic・NumPy・FastAPI 15:59 PgBouncerとpsqlODBC 17:25 SQLite 3.54.0 18:38 Ruby 4.1のプレビュー 19:39 Node.js更新と次のセキュリティ修正 20:51 VS CodeとVisual Studio 22:07 Cursor Originのアプリ連携 22:54 GitLabの緊急修正 23:57 Docker Hubの修正済み問題の開示 25:04 Lambdaの新しい実行モデルと接続維持 26:27 今週の確認リスト 公式出典 deno.com blog.python.org bun.com github.blog devblogs.microsoft.com github.blog devblogs.microsoft.com github.com/dotnet/core/blob/main/release-notes/10… nextjs.org github.com/expressjs/express/releases/tag/v5.3.0 github.com/fastify/fastify/releases github.com/fastify/fastify/security/advisories/GH… micronaut.io micronaut.io github.com/angular/angular/releases/tag/v22.2.3 github.com/vuejs/core/releases github.com/vuejs/core/blob/main/CHANGELOG.md github.com/vuejs/core/blob/minor/CHANGELOG.md github.com/nuxt/nuxt/releases/tag/v4.6.1 github.com/sveltejs/kit/releases/tag/@sveltejs%2F… github.com/withastro/astro/releases/tag/astro@7.4… github.com/rolldown/rolldown/releases/tag/v1.2.14 github.com/laravel/framework/releases/tag/v13.36.0 github.com/laravel/framework/releases shopify.dev github.com/pydantic/pydantic/releases/tag/v2.14.0 pydantic.dev github.com/pydantic/pydantic/releases github.com/numpy/numpy/releases/tag/v2.5.4 github.com/fastapi/fastapi/releases/tag/0.143.1 github.com/fastapi/fastapi/releases/tag/0.143.2 pgbouncer.org github.com/pgbouncer/pgbouncer/releases/tag/pgbou… postgresql.org sqlite.org ruby-lang.org nodejs.org nodejs.org nodejs.org openwall.com code.visualstudio.com learn.microsoft.com cursor.com docs.gitlab.com docker.com aws.amazon.com developers.cloudflare.com

    40 views
  10. AIが提案したパッケージ名を、インストール前にレジストリで確かめる PyPIとnpmの公開メタデータを使う はじめに AIが提案したコードに、見覚えのないパッケージ名が入っていたとき、そのまま pip install や npm install を打つ前に何を見ればよいか、という記事です。レジストリの公開メタデータに問い合わせる短いスクリプトと、実際の出力を載せています。 内容は4つです。なぜインストールの前なのか、スクリプトと出力、レビューへの組み込み方、この確認で分からないこと。スクリプトだけ持ち帰ってもらっても構いません。 掲載したスクリプトは、この記事のために書いたものです。使うのはPythonの標準ライブラリだけで、追加のインストールは要りません。 環境 macOS 26.6.2、Python 3.14.6、bash 3.2.57、git 2.54.0。2026年10月11日に実行しました。出力に出てくるバージョン番号や日付、リリース数は、その時点のものです。 なぜインストールの前に見るのか 生成AIは、実在しない情報をそれらしく出すことがあります。文章なら誤った一文で済みますが、コードの場合は、存在しないパッケージ名が import 文やインストール手順に入ります。 IPAの「情報セキュリティ10大脅威 2026」の解説書(組織編)は、AIが作り出したパッケージ名を悪用するサプライチェーン攻撃を取り上げています(Koi Securityの報告を引用)。AIが間違えて出しやすい名前を攻撃者が先に登録しておき、開発者がインストールするのを待つ、という手口です。スロップスクワッティング(slopsquatting)という非公式な呼び名もあります。同じ解説書には、AIが生成したコードに脆弱性が含まれる場合がある、という記述もあります。 確認をインストールの前に置く理由は単純で、PyPIでもnpmでも、インストールの過程でパッケージ側のコードが動くことがあるからです。入れてから調べる、では順番が逆になります。 この記事をざっくり図解 flowchart LR A["AIの提案に<br>知らないパッケージ名"] --> B["check_pkg.py"] B -->|公開メタデータを取得| C["PyPI / npm<br>レジストリ"] B --> D{"終了コード"} D -->|0| E["人がソースと<br>用途を見て判断"] D -->|1 要確認| E D -->|2 存在しない| F["入れない<br>正しい名前を調べ直す"] D -->|3 問い合わせ失敗| G["再実行"] 終了コード0でも、最後に判断するのは人です。スクリプトがやるのは、判断の材料を1画面に並べるところまでです。 1. スクリプト PyPIはJSON API(https://pypi.org/pypi/{名前}/json)、npmはレジストリ(https://registry.npmjs.org/{名前})に、認証なしでGETします。取り出すのは、初回公開日、最終公開日、リリース数、メンテナー、ソースの場所です。PyPIは最新版について登録されている既知の脆弱性も返すので、その件数も表示します。 #!/usr/bin/env python3 """インストール前に、パッケージ名をレジストリの公開メタデータで確かめる。 使い方: python3 check_pkg.py {pypi|npm} 名前 [名前 ...] 終了コード: 0=気になる点なし 1=要確認あり 2=存在しない名前あり 3=問い合わせ失敗 """ import json import os import re import sys import urllib.error import urllib.parse import urllib.request from datetime import datetime, timezone MIN_AGE_DAYS = int(os.environ.get("MIN_AGE_DAYS", 90)) # 初回公開からこの日数未満なら要確認 MIN_RELEASES = int(os.environ.get("MIN_RELEASES", 5)) # リリース数がこれ未満なら要確認 NAME_OK = re.compile(r"^(@[a-z0-9._-]+/)?[A-Za-z0-9._-]+$") def fetch(url): req = urllib.request.Request(url, headers={"User-Agent": "pre-install-check/0.1"}) with urllib.request.urlopen(req, timeout=15) as res: return json.load(res) def parse(ts): return datetime.fromisoformat(ts.replace("Z", "+00:00")) def pypi(name): d = fetch(f"https://pypi.org/pypi/{urllib.parse.quote(name)}/json") times = [parse(f["upload_time_iso_8601"]) for files in d["releases"].values() for f in files] urls = d["info"].get("project_urls") or {} roles = (d.get("ownership") or {}).get("roles") or [] return { "name": d["info"]["name"], "first": min(times) if times else None, "last": max(times) if times else None, "releases": sum(1 for files in d["releases"].values() if files), "latest": d["info"]["version"], "source": urls.get("Source") or urls.get("Repository") or urls.get("Homepage") or "(記載なし)", "maintainers": ", ".join(r["user"] for r in roles) or "(なし)", "vulns": len(d.get("vulnerabilities") or []), } def npm(name): d = fetch(f"https://registry.npmjs.org/{urllib.parse.quote(name, safe='@')}") t = d.get("time", {}) versions = d.get("versions", {}) repo = d.get("repository") or {} return { "name": d["name"], "first": parse(t["created"]) if "created" in t else None, "last": max((parse(t[v]) for v in versions if v in t), default=None), "releases": len(versions), "latest": d.get("dist-tags", {}).get("latest", "(なし)"), "source": repo.get("url", "(記載なし)") if isinstance(repo, dict) else str(repo), "maintainers": ", ".join(m["name"] for m in d.get("maintainers", [])) or "(なし)", "vulns": None, # このAPIの応答には含まれない } def check(registry, name): if not NAME_OK.match(name): print(f"[{registry}] {name}\n 名前の形式が不正です") return 3 try: m = {"pypi": pypi, "npm": npm}[registry](name) except urllib.error.HTTPError as e: if e.code == 404: print(f"[{registry}] {name}\n 判定: 存在しません(404)。インストールしないでください") return 2 print(f"[{registry}] {name}\n 判定: 問い合わせ失敗(HTTP {e.code})。存在しないとは限りません") return 3 except (urllib.error.URLError, TimeoutError) as e: print(f"[{registry}] {name}\n 判定: 問い合わせ失敗({e})。存在しないとは限りません") return 3 notes = [] if m["name"] != name: notes.append(f"レジストリ上の名前は {m['name']} です") if m["first"] is None or m["releases"] == 0: notes.append("公開済みのファイルがありません") else: age = (datetime.now(timezone.utc) - m["first"]).days if age < MIN_AGE_DAYS: notes.append(f"初回公開から{age}日(基準{MIN_AGE_DAYS}日未満)") if m["releases"] < MIN_RELEASES: notes.append(f"リリース数{m['releases']}(基準{MIN_RELEASES}未満)") if m["vulns"]: notes.append(f"最新版に既知の脆弱性の登録が{m['vulns']}件") day = lambda x: x.strftime("%Y-%m-%d") if x else "(不明)" print(f"[{registry}] {m['name']}") print(f" 初回公開 : {day(m['first'])}") print(f" 最終公開 : {day(m['last'])} (最新 {m['latest']})") print(f" リリース数 : {m['releases']}") print(f" メンテナー : {m['maintainers']}") print(f" ソース : {m['source']}") if m["vulns"] is not None: print(f" 既知の脆弱性: {m['vulns']}件(最新版、PyPIの登録分)") print(" 判定: " + ("要確認。" + " / ".join(notes) if notes else "気になる点なし(安全の証明ではありません)")) return 1 if notes else 0 if __name__ == "__main__": if len(sys.argv) < 3 or sys.argv[1] not in ("pypi", "npm"): print(__doc__, file=sys.stderr) sys.exit(3) sys.exit(max(check(sys.argv[1], n) for n in sys.argv[2:])) 書くときに決めたことを、3つだけ挙げます。 404と通信失敗を分ける。 社内プロキシやレジストリ側の不調で応答が取れなかったときに「存在しません」と出すと、実在するパッケージを疑うことになります。存在しないと言えるのは404が返ったときだけにして、それ以外は終了コード3にしました。 名前の形式を先に確かめる。 渡された文字列をそのままURLに入れるので、英数字と . _ -、npmのスコープ(@scope/name)以外は問い合わせる前に弾いています。 基準値は目安。 初回公開から90日未満、リリース数5未満を「要確認」にしていますが、この数字に根拠となるデータはありません。新しくて良いパッケージも当然あります。環境変数で変えられるようにしてあるので、チームで決めた値に置き換えてください。 2. 実行結果 よく知られたパッケージ python3 tools/check_pkg.py pypi requests [pypi] requests 初回公開 : 2011-02-14 最終公開 : 2026-05-14 (最新 2.34.2) リリース数 : 160 メンテナー : Lukasa, graffatcolmingov, nateprewitt ソース : https://github.com/psf/requests 既知の脆弱性: 0件(最新版、PyPIの登録分) 判定: 気になる点なし(安全の証明ではありません) python3 tools/check_pkg.py npm express [npm] express 初回公開 : 2010-12-29 最終公開 : 2026-10-09 (最新 5.3.0) リリース数 : 290 メンテナー : wesleytodd, jonchurch, ctcpip, ulisesgascon, sheplu ソース : git+https://github.com/expressjs/express.git 判定: 気になる点なし(安全の証明ではありません) 終了コードはどちらも0でした。メンテナーの欄は、PyPIでは ownership.roles、npmでは maintainers から取ったアカウント名です。最初はPyPIの author を出そうとしましたが、これは公開する側が自分で書く値でした。応答を見直して ownership があると分かり、そちらに替えています。 存在しない名前 説明用に作った名前で試します。2026年10月11日の時点で、PyPIにもnpmにも登録されていませんでした。 python3 tools/check_pkg.py pypi no-such-package-demo-20261011 [pypi] no-such-package-demo-20261011 判定: 存在しません(404)。インストールしないでください 終了コードは2です。npmでも同じ結果でした。AIの提案に入っていた名前でこれが出たら、AIが作った名前か、書き間違いの可能性があります。その名前は入れずに、やりたかったことから正しいパッケージを探し直します。 一つ注意があります。記事に載せた名前は、あとから誰でも登録できます。みなさんが試すときには、結果が変わっているかもしれません。この確認が「今の時点」のものでしかない、という実例でもあります。 要確認になる場合 実在の新しいパッケージを名指しするのは避けたかったので、基準値のほうを極端にして分岐を確かめました。 MIN_AGE_DAYS=100000 python3 tools/check_pkg.py pypi requests [pypi] requests 初回公開 : 2011-02-14 最終公開 : 2026-05-14 (最新 2.34.2) リリース数 : 160 メンテナー : Lukasa, graffatcolmingov, nateprewitt ソース : https://github.com/psf/requests 既知の脆弱性: 0件(最新版、PyPIの登録分) 判定: 要確認。初回公開から5717日(基準100000日未満) 終了コードは1です。試していて気づいた点が2つありました。 Requests と大文字で渡すと、PyPIは requests の情報を返します。PyPIは名前を正規化して扱うためです。スクリプトは、渡した名前とレジストリ上の名前が違うときも要確認にしているので、この場合は終了コード1になりました。表記の違いだけなら問題ありませんが、別の名前に解決されたことには気づけます。 urllib3 では、ソースの欄が「(記載なし)」になりました。リポジトリのURLは登録されているのに、項目名が Code で、スクリプトが見ている Source Repository Homepage のどれでもなかったためです。項目名はプロジェクトごとにばらつくので、「記載なし」は「リポジトリがない」という意味にはなりません。 3. レビューに組み込む 手元で1回打つだけでは、忘れた日に抜けます。依存を追加するプルリクエストで、追加された名前だけを取り出して確認する形にしました。 #!/usr/bin/env bash # 使い方: tools/check-new-deps.sh [比較元ブランチ] # requirements.txt に追加された行だけを取り出して、check_pkg.py に渡す set -euo pipefail base="${1:-main}" names=$(git diff "$base"...HEAD -- requirements.txt \ | grep -E '^\+[A-Za-z0-9]' \ | sed -E 's/^\+//; s/[^A-Za-z0-9._-].*$//' || true) if [ -z "$names" ]; then echo "requirements.txt に追加された依存はありません" exit 0 fi # shellcheck disable=SC2086 python3 tools/check_pkg.py pypi $names requirements.txt に urllib3>=2.0 と、先ほどの存在しない名前を追加したブランチで実行した結果です。 tools/check-new-deps.sh main [pypi] urllib3 初回公開 : 2009-12-11 最終公開 : 2026-09-15 (最新 2.8.0) リリース数 : 105 メンテナー : SethMichaelLarson, illia-v, shazow ソース : (記載なし) 既知の脆弱性: 0件(最新版、PyPIの登録分) 判定: 気になる点なし(安全の証明ではありません) [pypi] no-such-package-demo-20261011 判定: 存在しません(404)。インストールしないでください 終了コードは2でした。追加がないブランチでは「requirements.txt に追加された依存はありません」と出て、0で終わります。 出力はプルリクエストの説明欄に貼り、テンプレートに次の項目を足しておくと、レビューする側も同じものを見られます。 ## 依存の追加(ある場合のみ) - [ ] `tools/check-new-deps.sh` の出力を下に貼った - [ ] 追加した名前は、公式ドキュメントやリポジトリのREADMEに書かれたインストール名と一致している - [ ] AIの提案で知った名前の場合、提案以外の情報源でも確認した - [ ] 「要確認」が出たものは、追加する理由を1行書いた 2つ目の項目が、実はいちばん効きます。スクリプトが教えてくれるのは「その名前で何かが登録されている」ことまでで、「自分が使いたかったライブラリの正しい名前である」ことは、公式の情報と見比べないと分かりません。 終了コードを分けてあるので、CIで2のときだけ失敗させ、1は警告にとどめる、といった使い方もできます。ただし、CI上では実行していません。確かめたのは手元のgitリポジトリだけです。 この確認で分からないこと - 存在することは、安全の証明になりません。 攻撃者が先に登録した名前は「存在する」ので、存在確認だけなら通ります。新しさとリリース数を見ているのはそのためですが、基準を超えるまで待つ相手には効きません。 - 有名な名前に似せた名前は検出しません。 1文字違いの名前が実在すれば、その情報が普通に表示されます。 - 古くて実績のあるパッケージが、ある版から悪意のあるものに変わる場合も見えません。 見ているのは登録の履歴で、中身のコードは読んでいません。 - メタデータは公開する側が書きます。 ソースの欄に、他人のリポジトリのURLを書くこともできます。 - 直接指定した名前しか見ていません。 そのパッケージが連れてくる依存は対象外です。 - 利用者数は見ていません。 PyPIのJSON APIにはダウンロード数の項目がありますが、値は -1 で、公式ドキュメントにも使わないようにと書かれています。npmには別のAPIがありますが、スクリプトには入れていません。 - 既知の脆弱性の表示は、PyPIの最新版の件数だけです。 0件は「登録がない」という意味で、「脆弱性がない」という意味にはなりません。npm側は見ていません。依存全体を照合する監査ツールや、ロックファイルでの版とハッシュの固定は別の層の対策で、この記事では試していません。 試していない範囲も書いておきます。check-new-deps.sh が読むのは requirements.txt だけで、確かめた書式は 名前==版、名前>=版、名前のみ、コメント行です。-r での読み込み、URL指定、extras、環境マーカーは試していません。package.json やロックファイルの差分、プライベートレジストリやミラー、レジストリのレート制限も未確認です。既知の脆弱性が1件以上あるときに要確認になる分岐も、該当するパッケージで実行してはいません。 最後に AIが提案した名前は、インストールの前に一度レジストリに聞く。404なら入れない。存在していても、公式の情報と名前を見比べる。この記事のスクリプトは、その最初の一歩を手早く済ませるためのものです。まずは次に依存を追加するときに、1回だけ試してみてください。 参考文献 - IPA「情報セキュリティ10大脅威 2026」 ipa.go.jp - PyPI Docs: JSON API docs.pypi.org - npm: Public Registry API github.com/npm/registry/blob/main/docs/REGISTRY-A… - Python Packaging User Guide: Names and normalization packaging.python.org - 背景(シャドーAIとAI生成コードのリスクの整理)は、VAONのブログにまとめています。 vaon.com.vn

Achievements

Awards earned by the repositories expressjs owns. Select an award to embed its badge.

Gold
01st place finishes
Silver
02nd place finishes
Bronze
13rd place finishes
Awarded repositories
1Owned by expressjs now

Repositories

Ordered by gold, then silver, then bronze.

  1. expressBest: 3rd by stars, 621 stars001

Every award

Top-three finishes of these repositories in completed trending days and weeks, newest first.

  1. express

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.