One API Key, One Balance: Wiring Models and Data APIs into Claude Code, Cursor and Codex
Disclosure: this post is written by the OpenAny team. OpenAny is our product. We tried to keep it practical, and the limitations section is there on purpose.
The problem: every new tool is another key and another bill
A coding agent rarely stops at one model. In a typical week ours used a frontier model for planning, a cheap fast model for bulk edits, a search API to read the docs, and something to pull GitHub repository metadata. Every one of those meant:
- a new account and a new API key in .env,
- another prepaid balance or card on file,
- another usage dashboard to check when a bill looked wrong,
- another set of rate limits and error formats for the agent to learn.
Agents make this worse because they decide to call things on their own. "Which key paid for that call, and how much did it cost?" gets hard to answer quickly.
We built OpenAny for that case. It's a pay-per-call marketplace where models, data APIs and agent skills sit behind one API key and one prepaid balance. This post shows the actual setup in Claude Code, Cursor and Codex, the commands involved, and where it isn't a good fit.
What you get (and what you don't)
- Models through an OpenAI-compatible Chat Completions endpoint at https://api.openany.si/v1.
- Data and tool APIs (search, GitHub, finance, e-commerce, SEO and more), called through the openany CLI. Each listing shows a fixed price and a billing unit.
- Skills: portable SKILL.md workflows. They run inside your agent and call the models and APIs above. OpenAny doesn't host a skill runtime.
- Billing: one shared prepaid balance with no subscription. You can top up by card, USDC or x402, and card auto top-up and USDC subscriptions are both available. New accounts get $1 in signup credit that doesn't expire.
As of 2026-11-03 the published catalog lists 154 models, 3,138 API endpoints and 59 provider identities. Those are listing counts, not uptime claims.
There's no MCP server. The integration is a CLI, a skill file and plain HTTP endpoints.
Step 1: Get a key and make a first call
Create an account, then create an API key from the dashboard. Put the key in the environment of the process that runs your agent, not in a prompt:
export OPENANY_API_KEY="..." # better: your shell's secret manager or the client's secret store
Pick a model ID from the model catalog. Model IDs are route-specific, so copy the ID from the listing instead of guessing. The examples below use deepseek-v4-flash, which was listed when we wrote this.
cURL:
curl --fail-with-body --silent --show-error \
https://api.openany.si/v1/chat/completions \
-H "Authorization: Bearer $OPENANY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4-flash",
"messages": [{"role": "user", "content": "Reply with exactly: OpenAny connected"}],
"max_tokens": 32
}'
Python (official openai package):
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["OPENANY_API_KEY"],
base_url="https://api.openany.si/v1",
)
response = client.chat.completions.create(
model=os.environ.get("OPENANY_MODEL", "deepseek-v4-flash"),
messages=[{"role": "user", "content": "Reply with exactly: OpenAny connected"}],
max_tokens=32,
)
print(response.choices[0].message.content)
print(response.usage)
A successful call returns HTTP 2xx with a non-empty choices[0].message.content. The usage block tells you what you were billed for.
Some models have more than one provider route. The catalog listing shows the routes for each model.
Step 2: Install the CLI and the skill
Data APIs and skills go through the openany CLI (Node.js 22 or later):
npm install -g openany-cli@latest --registry=https://registry.npmjs.org/
openany --version
openany setup --scenario call # paste the key at the hidden prompt, never in chat
openany status --scenario call -j
openany balance -j
setup saves the key to ~/.config/openany/credentials.json with 0600 permissions. If OPENANY_API_KEY is set, it takes precedence over the saved key.
Next, install the OpenAny skill where your agent looks for skills. The file is the same for every client. Only the directory changes:
| Client | Skill location |
|---|---|
| Claude Code | .claude/skills/openany/SKILL.md |
| Cursor | .cursor/skills/openany/SKILL.md |
| Codex | .agents/skills/openany/SKILL.md |
# Example for Claude Code; swap the directory for Cursor or Codex
mkdir -p .claude/skills/openany
curl -fsSL https://openany.si/SKILL.md -o .claude/skills/openany/SKILL.md
Read the file before you install it. It's plain Markdown, and you should know what instructions you're handing to your agent. There's also a shortcut: paste Read openany.si/AGENT.md and connect OpenAny into the agent and let it walk through the same steps.
None of this replaces your agent's main model. Claude Code still uses whatever model you already run. The skill only lets the agent call external models and APIs through the CLI when a task needs them.
Step 3: Call a data API from your agent
Say your agent needs repository metadata such as stars, forks, language and license. Here's what the flow looks like with the CLI. Catalog browsing doesn't need a key:
openany discover -q "github repository" -l 5 --public -j
openany inspect --id 2a01d388-02fb-4307-8e47-f3a814d9a3cd --public -j
inspect returns the method, the input schema and the price. For this endpoint, the input is a required url in the request body, and the billing unit is a fixed price per call ($0.0012 when we checked). Then run it:
openany run --id 2a01d388-02fb-4307-8e47-f3a814d9a3cd \
-i '{"url": "https://github.com/facebook/react"}' \
--query '{"max_cost_usd": "0.01"}' -j
The response is the repository metadata as JSON, plus the request ID and the amount charged.
That max_cost_usd parameter caps the price for this one request. If no route can serve the call at or below that price, the request is refused before it runs and nothing is charged. The same endpoint accepts fields, max_items and jq query parameters, which shrink the response without changing the cost. That's useful when the agent doesn't need 40 KB of JSON back.
In practice you don't type these commands yourself. You give the agent a bounded task like this:
> Use the openany skill. Find an API that returns GitHub repository metadata, show me the price before calling, then fetch facebook/react. Budget: $0.05.
The skill tells the agent to check the balance, discover the API, inspect it, show the cost and ask before spending. After the call it should report the request ID and the charge. To check afterwards, run openany calls list --limit 20 -j or openany calls get --id REQUEST_ID -j.
Step 4 (optional): Use OpenAny as the main model provider
This is separate from tool access. Only do it if you actually want your editor's chat model billed through OpenAny.
Cursor. In Cursor Settings → Models, enter your OpenAny key in the OpenAI API key field. If your version shows Override OpenAI Base URL, set it to https://api.openany.si/v1. Then add an exact model ID from the catalog and send one short test message. Two caveats: Cursor limits own-key access to supported chat models, and Tab completion still uses Cursor's built-in models.
Codex. Merge a custom provider into your user-level ~/.codex/config.toml:
model = "MODEL_ID_FROM_LISTING"
model_provider = "openany"
[model_providers.openany]
name = "OpenAny"
base_url = "https://api.openany.si/v1"
env_key = "OPENANY_API_KEY"
wire_api = "responses"
Codex uses the Responses API, so the model you choose needs a route that supports Responses, streaming and tool calls. Chat Completions support alone doesn't guarantee that. Restart Codex, send one small request, and check the Usage page.
Check the model listing for Responses support before you switch.
Claude Code. Our documented Claude Code setup is the skill and CLI from Step 2, with Claude Code's own model left unchanged.
Cost control with a prepaid balance
Here's what actually keeps spending bounded:
1. It's prepaid. The worst case is your balance, not an open-ended invoice. There's no subscription, and the $1 signup credit is enough to test the setup.
2. Prices are visible before the call. Every API listing has a fixed price and a billing unit, and inspect returns them, so the agent can show the cost before spending.
3. There's a per-request cap. max_cost_usd on data APIs refuses an over-budget call instead of charging for it.
4. Usage is recorded per call. openany calls list shows a chargedCostMicroUsd for each row, and the dashboard has a usage page.
5. Auto top-up is opt-in. Card auto top-up and USDC subscriptions each need their own authorization. If you let an agent top up through the CLI, you set the network, the per-payment limit, the total limit and an expiry up front.
The pricing page explains billing units and the difference between balance-funded calls and direct x402.
Limitations: when not to use this
- If you only use one provider. If everything you run is on one vendor's API and their billing works for you, an extra layer only adds a hop.
- If you need a hosted MCP server. OpenAny doesn't publish one. The integration is the CLI, SKILL.md and HTTP.
- If your agent can't run a terminal. Data APIs and skills go through the Node CLI. An HTTP-only client can still use the model endpoint, but not the CLI workflow.
- If you need every protocol on every model. Chat Completions works across the catalog, but Responses and Anthropic Messages support depend on the route. Check before switching a main model.
- If you expect x402 everywhere. x402 funds your balance. Paying per request directly with x402 only works on endpoints that explicitly support it.
- If you need a long track record. We launched in October 2026. Listing counts aren't uptime guarantees, so try a few calls before building anything critical on top.
Wrapping up
The practical win is boring, and that's the point. Your agent gets one key, one balance and one usage log, and the main model stays the same unless you choose to change it. Setup is a CLI install, a skill file and an environment variable. If you try it, the docs have the full client recipes. We'd like to hear where it breaks, especially Cursor and Codex edge cases. You can reach us at team@openany.si.
If you run an API and want agents to be able to call it, you can list it on OpenAny for free and set your own prices. Payment and payout fees may still apply.
Here was the bug I eventually traced it to
react#26048
React has no dependencies:
npmjs.com
github.com/react/react/blob/main/package.json
If you read the first comment on your link, you'd see why you're wrong:
> That's not _React_, that's `create-react-app`
Not even a link to a gh issue about the root cause?
2 secs Google search: react#31819
Google Translate was crashing our React app — here's how we fixed "removeChild" for good
---
title: Google Translate was crashing our React app — here's how we fixed "removeChild" for good
published: true
description: Our most engaged user hit "Failed to execute 'removeChild' on 'Node'" because Chrome was auto-translating the page. Why it happens, how to reproduce it without Google Translate, and the three-layer fix we shipped.
tags: react, javascript, webdev, debugging
cover_image: dev-to-uploads.s3.us-east-2.amazonaws.com
---
I build QRflows, a dynamic QR code platform. Last week I was going through session recordings and found our most engaged user of the month: someone in Hong Kong who spent 11 minutes in the dashboard, clicked 82 times, tried half a dozen QR types, added a logo, built a landing page.
At minute three, the page crashed and reloaded.
The error in the console:
NotFoundError: Failed to execute 'removeChild' on 'Node':
The node to be removed is not a child of this node.
The recording made the cause obvious once I noticed it: the whole dashboard was in Chinese. Our site only exists in English. The user had Chrome's built-in Google Translate turned on.
This is one of those bugs that never shows up in your own testing, because you never translate your own app. Here's what's going on, how to reproduce it without Google Translate, and the three-layer fix we shipped.
---
Why Google Translate breaks React
React keeps references to the DOM nodes it created, including plain text nodes. When state changes, it updates or removes those exact nodes.
Google Translate doesn't know about React. When it translates a page, it walks the DOM and replaces text nodes with its own <font> elements containing the translated text:
<!-- What React rendered -->
<button>Save</button>
<!-- What the DOM looks like after Google Translate -->
<button><font><font>保存</font></font></button>
The original "Save" text node is gone from the DOM, but React still holds a reference to it. The next time React needs to remove or replace that text, it calls parent.removeChild(textNode) — and the browser throws, because that node is no longer a child of anything.
The same thing happens with insertBefore when React tries to insert something next to a node that Translate has swapped out.
It's a long-standing issue — facebook/react#11538 has been open since 2017 — and it isn't specific to React: anything that holds references to text nodes is vulnerable. If you want the deep dive, Martijn Hols' write-up is excellent.
Which code is vulnerable
The crash needs two things: a bare text node and something conditional next to it. Patterns like these:
// Text sitting next to a conditional element
<button>
{isSaving && <Spinner />}
Save
</button>
// Conditional text
{error && error.message}
// Ternary returning strings
<span>{isSaving ? "Saving…" : "Saved"}</span>
// Text mixed with an expression
<p>Uploaded {count} files</p>
Every QR builder has dozens of these: status labels, counters, "Uploading… / Done", plan badges. Which is why the crash hit our most active user and nobody else.
---
Reproducing it without Google Translate
Waiting for a translated session to show up in your logs is not a test plan. You can simulate what Translate does to the DOM in the browser console:
function fakeTranslate(root = document.body) {
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode: (node) =>
node.nodeValue.trim() &&
node.parentElement &&
!node.parentElement.closest('script, style, [translate="no"], font')
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_REJECT,
});
const nodes = [];
while (walker.nextNode()) nodes.push(walker.currentNode);
for (const textNode of nodes) {
const outer = document.createElement('font');
const inner = document.createElement('font');
inner.textContent = '译' + textNode.nodeValue; // mark it as "translated"
outer.appendChild(inner);
textNode.parentNode.replaceChild(outer, textNode);
}
return nodes.length;
}
window.addEventListener('error', (e) => console.log('CRASH:', e.message));
fakeTranslate();
Paste it, run fakeTranslate(), then use the app: switch tabs, toggle options, type into inputs, navigate between routes. Run fakeTranslate() again after each step, because real Translate keeps re-translating new content as it appears.
On our dashboard, switching QR types after this reproduced the exact crash from the recording within a few clicks.
---
The fix: three layers
There's no single clean fix. What worked for us is three layers, from "never crash" to "fewer weird states".
Layer 1: a DOM guard so the crash can't happen
This is the workaround from the React issue thread: patch removeChild and insertBefore so that, instead of throwing, they skip the operation when the node relationship is no longer what React expects.
// src/lib/domTranslateGuard.ts
export function installDomTranslateGuard(): void {
if (typeof window === 'undefined' || typeof Node !== 'function') return;
const proto = Node.prototype as any;
if (proto.__translateGuardInstalled) return;
proto.__translateGuardInstalled = true;
const originalRemoveChild = proto.removeChild;
proto.removeChild = function (child: Node) {
if (child.parentNode !== this) {
console.warn('[dom-guard] removeChild skipped', child, this);
(window as any).clarity?.('event', 'dom_guard_removeChild');
return child;
}
return originalRemoveChild.call(this, child);
};
const originalInsertBefore = proto.insertBefore;
proto.insertBefore = function (newNode: Node, referenceNode: Node | null) {
if (referenceNode && referenceNode.parentNode !== this) {
console.warn('[dom-guard] insertBefore skipped', referenceNode, this);
(window as any).clarity?.('event', 'dom_guard_insertBefore');
return newNode;
}
return originalInsertBefore.call(this, newNode, referenceNode);
};
}
Two details that matter:
Install it before React touches the DOM. We call it as the very first thing in the client entry, before hydrateRoot:
// entry-client.tsx
import { installDomTranslateGuard } from './lib/domTranslateGuard';
installDomTranslateGuard();
// ...then hydrateRoot(...)
Keep it out of SSR. It only lives in the client entry, and the typeof window check is a second line of defence. The server never touches Node.prototype.
The trade-off is honest: this hides the error instead of fixing the cause. A skipped insertBefore can occasionally leave a piece of UI missing until the next render. That's why we log it and send a custom event to our session analytics — so we can see how often the guard actually fires, and on which pages.
Layer 2: stop giving Translate bare text nodes
The guard prevents crashes. Wrapping text in elements prevents most of the situations where it would have to step in. If text lives inside its own <span>, Translate replaces the text inside the span, and React only ever adds or removes the span itself — which is still there.
// Before
<button>
{isSaving && <Spinner />}
Save
</button>
// After
<button>
{isSaving && <Spinner />}
<span>Save</span>
</button>
// Before
{error && error.message}
// After
{error && <span>{error.message}</span>}
// Before
<p>Uploaded {count} files</p>
// After
<p><span>Uploaded {count} files</span></p>
No visual change, no new classes. We went through every component in the QR builder, the file upload flow, the editor and the download modals, and wrapped anything that changes at runtime: status labels, counters, button text next to spinners.
Layer 3: translate="no" — but only where it makes sense
The tempting shortcut is <html translate="no">. Don't. Your users turned on translation because they need it — breaking the crash by breaking the translation just moves the problem.
We only opt out content that shouldn't be translated anyway:
- URL, email, phone and password inputs
- short links and destination URLs
- the QR preview and technical labels (sizes, PNG/SVG)
- the brand name in the header and footer
<input type="url" translate="no" />
<span translate="no">{shortUrl}</span>
Everything else — headings, buttons, help text — still gets translated.
---
How we verified it
After deploying, I ran the fake-translate script on the live site: the public PDF QR generator, then the logged-in dashboard. I switched QR types back and forth, toggled the logo option, typed into inputs, and navigated from the builder to the landing page editor and back — re-running fakeTranslate() after each step.
Result: zero uncaught errors, no reloads, and the UI kept updating live with the "translated" text in place. The brand name and URL fields stayed untouched, as intended.
We'll keep watching the dom_guard_* events in session analytics. If they show up a lot on a specific page, that page still has bare text nodes to wrap.
---
TL;DR
- Google Translate replaces text nodes with <font> elements. React still holds the old nodes and crashes on removeChild / insertBefore.
- You can reproduce it without Translate by wrapping text nodes in <font> from the console.
- Layer 1: patch removeChild / insertBefore before hydration, client-only, and log when it fires.
- Layer 2: wrap conditional and dynamic text in <span>.
- Layer 3: translate="no" only on inputs, URLs and brand names — never on the whole app.
The part that stuck with me: this bug hit the single most engaged user we had that month. International users who translate your app are often exactly the people working hardest to use it.
---
I'm building QRflows in public — dynamic QR codes with Smart Rules, real-time analytics and a Claude MCP integration. If you've hit this bug with a different framework, I'd like to hear how you handled it.
1 reaction · 2 comments
@rickyfm @acemarke @infinterenders medium.com
Wrote about it a while back, useState seems to call the basicStateReducer
github.com/react/react/blob/v19.2.0/packages/reac…
1 like · 191 views
新卒エンジニアが書き溜めた技術メモ 4選
はじめに
こんにちは、namonaki です。新卒で入社して半年、分からなかったことを個人用のメモに書き留めてきました。その中からいくつか選りすぐって、改めて調べつつ記事にしてみました。
■ 対象
Rails や React でコードは書けるが、「なぜこう書くと動くのか」はまだ説明できない人
普段なんとなく使っている仕組みの裏側を、ソースコードや DevTools で確かめてみたい人
■ 扱うこと
ブラウザ・Web標準
React と Islands Architecture
Rails
テスト
各章は独立しているので、気になるところから読んでください。参考にした資料は、本文中の番号から末尾の脚注に飛べます。
!
前提のバージョン(2026年10月時点)
Ruby 4.0 / Rails 8.1
React 17 / Node.js 24
factory_bot 6.5
1. ブラウザ・Web標準
JSON-LD(検索エンジン向けの構造化データ)について調べていたとき、JS を書くわけでもないのに、なぜ <script> タグを使うのか疑問に思ったのがきっかけです。
1.1 <script> の属性なし / async / defer の違い
調べてみると、HTML の要素を探して操作する JS には、defer を付けるのが基本でした。ダウンロードは早く始めつつ、実行は HTML の解析が終わるまで待ってくれるからです。
<script src="a.js"></script> <!-- ① 属性なし -->
<script src="a.js" async></script> <!-- ② async -->
<script src="a.js" defer></script> <!-- ③ defer -->
HTMLの解析を止めるか
実行タイミング
実行順
属性なし
止める
読み込んだらすぐ
書いた順
async
ダウンロード中は止めない
ダウンロードが終わりしだい
保証なし
defer
止めない
HTMLの解析が終わった後
書いた順
defer は「ダウンロードは早く、実行は最後」という良いとこ取り1
念のため、コード側でも DOMContentLoaded を待つと二重の保険になる
querySelectorAll のように要素が見つからなくてもエラーにならない書き方では、何も起きないだけなので気づきにくい。defer を付けておけば、この「要素がまだ無い」事故を防げる
1.2 Webのルールは誰が決めているのか
調べてみると、Web のルールは1つの団体が決めているわけではなく、部分ごとに別の団体が決めていました。
パンくずリストを構造化データで書いたときの例で見てみます。
<script type="application/ld+json">
{
"@context": "schema.org",
"@type": "BreadcrumbList",
"itemListElement": [ ... ]
}
</script>
この数行でも、部分ごとにルールを決めている団体が違いました。
部分
ルール
決めている団体
<script> タグ
HTML
WHATWG
application/ld+json
MIMEタイプの仕組み / 登録
IETF / IANA
{ ... } の書き方
JSON
IETF・Ecma International
@context などの書き方
JSON-LD
W3C
BreadcrumbList という単語
語彙
schema.org
WHATWG:HTML と DOM。ブラウザを作る企業が中心。2019年に W3C と合意し、HTML は「HTML Living Standard」に一本化。版の区切りはなく更新され続ける2
W3C:CSS、アクセシビリティ(WAI-ARIA)、JSON-LD など3
IETF:HTTP や JSON など、通信とデータ形式の基礎(RFC)4
IANA:MIMEタイプの一覧、ドメイン名、IPアドレスの登録・管理5
Ecma International:JavaScript の文法(ECMAScript)6
schema.org:Google・Microsoft・Yahoo! などによる共同プロジェクト。標準化団体ではなく、構造化データ用の単語帳を作っている7
■ 補足
+json は「中身は JSON 形式」という意味の接尾辞8
<script> の type に JS 以外を指定すると、ブラウザは実行せずデータとして置いておく。検索エンジンはこれを読む
2. React と Islands Architecture
Wikipedia のような辞書的なページは、全体を SPA にするとかえって重くなるので、一部分だけを React で動くようにすることがあると思います。こうした構成にも何か名前が付いているはずだと思い、調べてみたのがきっかけです。
2.1 「一部だけReact」すなわち Islands Architecture …ではなかった
最初は、一部だけ React を使う構成ならすべて Islands Architecture だと思っていましたが、サーバーが空の div を返し、ブラウザで React が中身を一から作る構成は、Islands Architecture ではありませんでした。Islands では島の HTML もサーバーが作り、ブラウザはそれを hydrate するからです。
■ Islands Architecture の条件9
島(インタラクティブな部分)の初期 HTML もサーバーが描画する
ブラウザはその HTML に hydrate(既存の要素を再利用して、イベントや状態を結び付ける)する
島の外には JS を送らない
■ 空の div にマウントする構成(React 17 の例)
// サーバーが返すのは空の div だけ。ブラウザ側で中身を一から作る(render)
document.querySelectorAll('.search-widget').forEach((el) => {
// サーバーからの値は data-params 属性で受け取る
ReactDOM.render(<SearchWidget params={JSON.parse(el.dataset.params)} />, el);
});
サーバーが返す HTML と、JS 実行後の HTML を並べるとこうなります。
【render(空の div にマウントする構成)】
サーバーが返したHTML JS 実行後
<div class="search-widget"> <div class="search-widget">
(空っぽ) → <form>...</form> ← JS が一から作る
</div> </div>
【hydrate(Islands Architecture)】
サーバーが返したHTML JS 実行後
<div> <div>
<form>...</form> → <form>...</form> ← 要素は作り直さず、イベントや状態を結び付ける
</div> </div>
見分け方:JS を切ったときに島の中身が見えるか
React 17 なら ReactDOM.render が render、ReactDOM.hydrate が hydrate10
React 18 以降は、createRoot().render() と hydrateRoot() に置き換わっている11
「島の外には JS を送らない」点だけは Islands と共通
2.2 hydrate で起きていること
調べてみると、hydrate は、サーバーが作った要素をそのまま使い、クリック時の処理や状態を後から結び付ける処理でした。
サーバーから届いた HTML は「見た目」だけで、ボタンを押しても何も起きません。素の JS で、後から動くようにするとこうなります12。
<button id="like">いいね 0</button>
// 既にあるボタンを探して、処理を後付けする(要素は作り直さない)
const button = document.getElementById('like');
let count = 0;
button.addEventListener('click', () => {
count += 1;
button.textContent = いいね ${count};
});
これが「既存の HTML にイベントを結び付ける」いちばん素朴な形
React の hydrate も考え方は同じで、中では次のことをしている
サーバーが返したHTML(見た目だけ。押しても動かない)
<form><input><button>検索</button></form>
↓ ReactDOM.hydrate(<SearchForm />, el)
① コンポーネントを実行して、「本来こうなるはず」の設計図をメモリ上に作る
② 設計図と既存の要素を先頭から照らし合わせ、一致した要素を「この部品の担当」として紐付ける
③ onClick などのイベント処理を受け付けられるようにする
④ useState の状態や useEffect を動かし始める
↓
見た目は同じまま、押せば動く状態になる
設計図と HTML が食い違うと、React 17 は開発モードで警告を出す。テキストの違いは書き換えて合わせるが、属性の違いは直らないことがあるので、食い違いはバグとして直す10:1
②で作る「設計図 ↔ 実際の要素」の対応表は、正確には Fiber というオブジェクトで、React はこれを使って書き換える要素や、押されたボタンの担当を見つけている13
■ ブラウザで確かめる
Fiber が実際の DOM 要素に付いている様子は、React で作られたサイトなら DevTools で見られます。ここでは React 公式サイトの react.dev を使います。
Chrome で react.dev を開き、DevTools を開く(Mac なら ⌘ + Option + I)
DevTools の左上にある矢印のアイコン(要素を選ぶボタン)を押してから、「Learn React」ボタンをクリックする。矢印を押さずにクリックすると、リンク先へページが移動してしまう
Console タブで $0(Elements タブで選択中の要素)を実行し、<a … aria-label="Learn React" …>Learn React</a> が表示されることを確かめる
次を実行する
Object.keys($0)
// => ['reactFiber$9s34k04vvrf', 'reactProps$9s34k04vvrf']
// (2026年10月時点。$ の後ろの文字列はページを開くたびに変わる)
$0[Object.keys($0).find(k => k.startsWith('__reactProps$'))]
// => {className: 'w-full sm:w-auto …', aria-label: 'Learn React', target: '_self', ref: ƒ, onClick: ƒ, …}
reactFiber$… が Fiber への参照、reactProps$… が props。props の中に onClick の関数が入っているのが見える
react.dev は React 18 以降で作られているが、キーの名前は React 17 と同じ
これらは React 内部の仕組みで、公開された機能ではない。観察だけにとどめ、自分のコードから読み書きしない
2.3 render と hydrate の違いのまとめ
違うのは「要素を一から作るか、サーバーが作ったものを再利用するか」だけでした。Fiber との紐付け(2.2 の②)、イベントの受け付け(2.2 の③)、状態や副作用の開始(2.2 の④)は、どちらも同じです。
ここに「ページの一部か全体か」という範囲の軸を加えると、よく聞く構成は次の4つに整理できます。
render(要素を一から作る)
hydrate(サーバーが作った要素を再利用)
ページの一部
空の div にマウントする構成
Islands Architecture
ページ全体
SPA
サーバーで描画して hydrate(Next.js など)
どのマスでも、マウントした要素より内側は Fiber と紐付く。違うのは「どこまでを React に任せるか」と「要素を作るか、再利用するか」だけ
SPA もページ全体が Fiber と紐付くが、サーバーが返すのは空の div なので、hydrate ではなく render
Islands との違いは hydrate する範囲で、Islands は島ごとに別々の React として hydrate し、島の外は React と無関係の HTML のまま。Next.js はページ全体(document)を1つの React として hydrate する
3. Rails
Rails のチュートリアル系のサイトや動画で「コントローラーで @ を付けた変数は、ビューでも @ を付けて参照できる」と聞いたものの、裏で何が起きているのか分からず、Rails を学び始めてすぐに疑問に思ったのがきっかけで調べました。
3.1 コントローラーのインスタンス変数がビューに渡る仕組み
コントローラーのインスタンス変数がビューで使えるのは、Rails がビュー用のオブジェクトに書き写しているからでした。さらに、テンプレートはそのオブジェクトのメソッドに変換されて実行されていました(Rails 8.1 のソースより)14。
class UsersController < ApplicationController
def show
@user = User.find(params[:id]) # @ 付きならビューで使える
posts = @user.posts # @ なしはこのメソッドの中だけ
end
end
3.2 そもそも、なぜ書き写す必要があるのか
インスタンス変数は、それを持っているオブジェクトの中からしか見えません15。
@user を持っているのは、UsersController のインスタンス
テンプレートを実行するのは、それとは別のビュー用オブジェクト
別々のオブジェクトなので、ビューで @user を使うには、ビュー用オブジェクト側にも同じインスタンス変数が必要
そこで Rails が、コントローラーの変数を1つずつビュー用オブジェクトに書き写している
3.3 全体の流れ
ブラウザから GET /users/1(ユーザーの詳細画面)のリクエストが届く
↓ ルーティング(resources :users)が users#show に振り分け、params[:id] = "1" にする
① コントローラーを new して、show アクションを実行する
↓ @user = User.find(1)
↓ render(書かなくても自動で呼ばれる)
② 集める view_assigns … コントローラーのインスタンス変数をハッシュにする
↓ { "user" => #<User id: 1> }
③ 作る view_context … ビュー用オブジェクトを new する(ハッシュとコントローラー自身を渡す)
(new するクラスの正体は ④ で補足)
↓
⑤ 書き込む assign … ハッシュの中身を、ビュー用オブジェクトのインスタンス変数にする
↓
⑥ テンプレート実行 テンプレートを「ビュー用オブジェクトのメソッド」に変換して呼ぶ
→ テンプレートの @user は、ビュー用オブジェクトの @user
以下、順に見ていきます。
① コントローラーはリクエストごとに new される
ソースを読むと、Rails はリクエストが届くたびにコントローラーを new していました。ルーティングが users#show と判断すると、UsersController.dispatch というクラスメソッドが呼ばれ、その中で new されます(actionpack action_controller/metal.rb)。
def self.dispatch(name, req, res)
...
new.dispatch(name, req, res) # UsersController.new して、そのインスタンスで処理する
end
Web サーバー(Puma など)がリクエストを受け取る
↓ Rails.application.call(env)
Rack ミドルウェア(Rails の中で順番に呼ばれる前処理の部品。セッション・Cookie など)
↓
ActionDispatch::Routing::RouteSet#call … ルーティング
↓ Journey::Router#serve … GET /users/1 に合うルートを探す
↓ RouteSet::Dispatcher#serve … "users" → UsersController クラスに変換
UsersController.dispatch(:show, req, res) … Metal に定義されたクラスメソッド
↓ new.dispatch(:show, req, res) … self.new = UsersController.new
UsersController のインスタンス#dispatch … 以降はこのインスタンスの上で動く
Rack は、Ruby の Web サーバー(Puma など)とアプリ(Rails など)をつなぐ共通ルール。「call(env) を受け取って [ステータス, ヘッダー, 本文] を返す」という約束だけを決めている
env は、リクエストの情報(URL、HTTP メソッド、ヘッダーなど)を詰めたハッシュ
Puma・Rack ミドルウェア・Rails は別々のサーバーではなく、同じ1つのプロセスの中で動いている
リクエストごとに別のインスタンスなので、別のユーザーのリクエストと @user が混ざることはない
② 集める:view_assigns
actionpack abstract_controller/rendering.rb のメソッドです。
show の実行後に呼ばれたときの値を、コメントで書き込んでいます。
def view_assigns
variables = instance_variables - _protected_ivars
instance_variables => [:@_request, :@_response, :@_params, :@_routes, ..., :@user] ← Rails 内部用も混ざっている
配列の引き算で内部用を外す => [:@user]
variables.each_with_object({}) do |name, hash| # name = :@user、hash は最初は空の {}
hash[name.slice(1, name.length)] = instance_variable_get(name) # "@user" の2文字目以降 "user" をキーに、@user の値を詰める
end
=> { "user" => #<User id: 1> }
end
使っているのは、Ruby の標準メソッド2つだけでした16。
メソッド
何をするか
instance_variables
そのオブジェクトが持つインスタンス変数の名前を、シンボルの配列で返す
instance_variable_get(名前)
名前を指定して、その変数の値を取り出す
instance_variables はレシーバーを省略しているので、self.instance_variables と同じ。self は①で new されたコントローラーのインスタンス
除外する _protected_ivars は、@_params や @_request など、Rails 内部用の変数を並べた一覧
each_with_object({}) は、空のハッシュを用意して1件ずつ詰め、最後にそのハッシュを返すメソッド
つまり、コントローラーの中身を一覧して、@ 付きの変数を「名前 → 値」のハッシュに変換しているだけでした。
③ 作る:view_context
actionview action_view/rendering.rb のメソッドです。
def view_context
view_context_class.new(lookup_context, view_assigns, self)
end
第1引数:どのテンプレートファイルを使うかを決める条件(探すフォルダ、言語、html / json などの形式)
第2引数:②で作ったハッシュ
第3引数:コントローラー自身(self)
■ self はなぜコントローラーなのか
view_context は actionview の ActionView::Rendering モジュールに書かれていますが、それでも self はコントローラーでした。実行時の self は、「そのメソッドがどのファイルやモジュールに書かれているか」ではなく、「ドットの左側にいたオブジェクト(レシーバー)」で決まるからです17。
module Greeting
def who = self
end
class UsersController
include Greeting # Greeting のメソッドを、UsersController のインスタンスから呼べるようにする
end
c = UsersController.new
c.who.equal?(c) # => true(レシーバーの c が self になる)
UsersController.ancestors.first(2) # => [UsersController, Greeting]
include は、メソッドをコピーするのではなく、メソッドを探す経路(ancestors)にモジュールを追加する
c.who と呼ぶと、Ruby は UsersController → Greeting の順に who を探し、見つけたメソッドを c の上で実行する
Rails の view_context も同じで、コントローラーに include されたモジュールのメソッドです。①で new したコントローラーのインスタンスから、レシーバーを省略して(self.〜 として)呼ばれ続けるので、self はずっとそのインスタンスのままです。そのため第3引数の self には、リクエストを処理しているコントローラー自身が渡されます。
④ ビュー用のクラスは「無名クラス」
new しているクラス view_context_class は、ActionView::Base を継承した無名クラス(名前のないクラス)でした。
■ 無名クラスとは
Ruby では Class.new でクラスを作れます18。それを定数(大文字で始まる名前)に入れると名前が付き、入れなければ無名クラスになります。
Dog = Class.new { def hi = "hi" }
dog = Class.new { def hi = "hi" }
Dog.name # => "Dog"
dog.name # => nil(中身は同じでも、ローカル変数に入れただけでは名前が付かない)
dog.new.hi # => "hi"(変数 dog を通して、普通のクラスと同じように使える)
違うのは名前の有無だけで、継承・include・メソッド定義・new は普通のクラスと同じようにできる
無名クラスでも、使い回すには変数などに入れておく必要がある。ただし変数はクラスを入れておく箱にすぎず、変数名はクラスの名前にならない。箱は何個あってもよく入れ替えもできるので、クラス側には「自分がどの名前で呼ばれているか」が分からない。名前として記録されるのは、最初に入れた定数だけ
Rails はビュー用の無名クラスを、コントローラーのクラス側に @view_context_class として入れてキャッシュし、③の view_context_class.new(...) で取り出して new している
■ ヘルパーが呼べる理由
Rails は、この無名クラスにルーティングのヘルパーとアプリのヘルパーを include しています(action_view/rendering.rb)。
Class.new(klass) do
include routes.url_helpers(supports_path) # users_path など
include helpers # app/helpers/ のメソッド
end
ビュー用オブジェクトはこのクラスのインスタンスなので、ビューで users_path や自作のヘルパーが呼べる
ancestors をたどると、無名クラス → ヘルパーのモジュール → … → ActionView::Base の順にメソッドを探している
⑤ 書き込む:initialize と assign
③の new を呼ぶと、Ruby は次の2つを順に行います。
メモリ上に空のオブジェクトを作る
そのオブジェクトの initialize を、new と同じ引数で自動的に呼ぶ
action_view/rendering.rb
view_context_class.new(lookup_context, view_assigns, self)
↓ ↓ ↓
action_view/base.rb ↓ ↓ ↓
def initialize(lookup_context, assigns, controller)
@lookup_context = lookup_context
...
assign_controller(controller) # @_controller にコントローラーを入れる
...
assign(assigns) # 最後に、②のハッシュを書き込む
end
def assign(new_assigns)
@_assigns = new_assigns
new_assigns.each { |key, value| instance_variable_set("@#{key}", value) }
end
assign は、②の view_assigns のちょうど逆の操作でした。
② コントローラー側
⑤ ビュー側
使うメソッド
instance_variable_get
instance_variable_set
名前の変換
:@user → "user"
"user" → "@user"
結果
変数 → ハッシュ
ハッシュ → 変数
instance_variable_set("@user", value) は、@user = value を、変数名を文字列で指定して書いたもの19
@user = value と書くと変数名はコードを書いた時点で決まるが、instance_variable_set なら実行中に決められる
Rails は開発者がどんな名前の変数を作るか知らないので、ハッシュのキーから名前を組み立てて代入している
2つのオブジェクトの中身の移り変わりを図にすると、こうなります。
【コントローラー】 @_params, @user=#<User name: "山田"> (show を実行した直後)
│
│ ② view_assigns:内部用を除いて、名前と値のハッシュにする
▼
{ "user" => #<User name: "山田"> }
│
│ ③ ビュー用のクラス.new(ハッシュ, self)
▼
【ビュー用オブジェクト】 (まだ空っぽ)
│
│ ⑤ initialize の中で
│ assign_controller(self) … @_controller にコントローラーを入れる
│ assign(ハッシュ) … "user" を "@user" にして instance_variable_set
▼
【ビュー用オブジェクト】 @_controller, @user=#<User name: "山田">
│
│ ⑥ テンプレートを実行
▼
<p>山田さん</p>
⑥ テンプレートは「ビュー用オブジェクトのメソッド」になる
ここでいうテンプレートは、app/views/ 配下のファイル全部のことです。
種類
ファイル名の例
アクションのテンプレート
users/show.html.erb
partial(部品のテンプレート)
users/_form.html.erb
レイアウト
layouts/application.html.erb
_ で始まるのは partial だけで、どれも同じ仕組みで動きます。ここでは show.html.erb を例にします。
<p><%= @user.name %>さん</p>
Rails はこのファイルを、次の Ruby のメソッドに変換していました。rails runner で ActionView::Template を作り、compiled_source(action_view/template.rb)を表示させた結果です。実際の出力は1行にまとまっているので、見やすく改行してコメントを足しています。
def _app_views_users_show_html_erb__1476774381686002440_15872(local_assigns, output_buffer, &_)
@virtual_path = "users/show";
@output_buffer.safe_append='<p>'.freeze; # 固定の HTML はそのまま足す
@output_buffer.append=( @user.name ); # <%= @user.name %> は、式の結果を(エスケープして)足す
@output_buffer.safe_append='さん</p>'.freeze;
@output_buffer # 組み立てた HTML を返す
end
実行したコード
rails runner で、テンプレートの中身を直接渡して変換させました。ファイルを作ったり DB を書き換えたりはしないので、どの Rails アプリでも試せます。
bundle exec rails runner '
template = ActionView::Template.new(
"<p><%= @user.name %>さん</p>", # テンプレートの中身
"app/views/users/show.html.erb", # ファイルのパス(実在しなくてよい)
ActionView::Template.registered_template_handler(:erb),
locals: [],
format: :html,
virtual_path: "users/show"
)
puts template.send(:compiled_source) # Ruby のメソッドに変換した結果を表示
'
compiled_source は外から呼べない private メソッドなので、send で呼んでいる
メソッド名の末尾の数字は、実行するたびに変わる
テンプレートは、ファイルのまま実行されるのではなく、1つの def に変換される
メソッド名は、ファイルのパスと番号から自動で作られる。先頭の _ は他のメソッドと名前がぶつからないための接頭辞で、partial かどうかとは関係ない
Haml でも、生成されるコードの中身が違うだけで仕組みは同じ
■ 変換から実行までの3段階
show.html.erb(ただのテキストファイル)
│ 1. 変換:テンプレートエンジン(ERB / Haml)が "def ... end" という Ruby の「文字列」にする
▼
"def _app_views_users_show_html_erb...(local_assigns, output_buffer, &_) ... @user ... end"
│ 2. 定義:module_eval で読み込ませる → ビュー用のクラスで使える本物のメソッドになる
▼
ビュー用のクラスのメソッド _app_views_users_show_html_erb...
│ 3. 実行:ビュー用オブジェクト.public_send(:_app_views_users_show_html_erb__...)
▼
メソッドの中の self = ビュー用オブジェクト
├ @user → self の @user(⑤で assign した値)
└ users_path → self のメソッド(④で include されたヘルパー)
module_eval(文字列) は、文字列のコードをクラスの中に書いたものとして実行する。"def ... end" を渡せば、そのクラスにメソッドが追加される
public_send(:foo) は、メソッド名をシンボルで指定して呼ぶメソッド。obj.public_send(:foo) は obj.foo と同じ
テンプレートのメソッド名は実行中に自動で作られるので、obj._app_views_... のようにコードへ直接書いておけない。そのため、名前を値として受け取れる public_send で呼んでいる
klass = Class.new
klass.module_eval("def greet = 'こんにちは'") # 文字列のコードから greet メソッドを定義する
obj = klass.new
name = :greet # 呼びたいメソッドの名前を、値として持っている
obj.public_send(name) # => "こんにちは"(obj.greet と同じ)
1と2は最初の1回だけ。2回目以降のリクエストは、定義済みのメソッドを呼ぶだけなので速い
テンプレートの @user が読めるのは、Ruby に「メソッドの中の @xxx は、そのメソッドを呼ばれたオブジェクト(self)の変数を指す」というルールがあるから(③の self の話と同じ考え方で、つまり今回はビュー用オブジェクトの @user を指す)
3.4 この仕組みから分かること
書き写されるのは、オブジェクトへの参照でした。中身を丸ごと複製(ディープコピー)しているわけではありませんでした。
Ruby の変数に入っているのは、オブジェクトそのものではなく「そのオブジェクトがどこにあるか」を指す参照です。②⑤の書き写しは、この参照を受け渡しているだけで、オブジェクトを複製する処理(dup など)はどこにもありません。
ref = instance_variable_get(:@user) # ② コントローラーの @user から参照を取り出す
hash["user"] = ref # ハッシュに入れる(複製しない)
instance_variable_set("@user", ref) # ⑤ ビュー用オブジェクトの @user に入れる(複製しない)
そのため、2つの @user は同じオブジェクトを指します。
[コントローラー] @user ──┐
├──→ #<User name: "山田">(メモリ上に1つだけ)
[ビュー用オブジェクト] @user ──┘
現象
理由
ビューで @user = 別のもの と代入し直しても、コントローラー側は変わらない
変数は別々で、ビュー側の矢印だけが別のものを指し直すから
ビューで @user.name = "x" と中身を変えると、コントローラー側から見ても変わる
2つの変数が同じオブジェクトを指しているから
partial でも @user が使える
partial も同じビュー用オブジェクトのメソッドとして実行されるから
ビューでは params も使える
ビュー用オブジェクトは params のデータを持たず、⑤で @_controller に入れたコントローラーの params を呼んでいるから(delegate による委譲)
@usr と書き間違えてもエラーにならず、nil になる
Ruby では、作っていないインスタンス変数を読むと nil が返るから
4. テスト
いろいろなテストコードを読むうちに、FactoryBot には create だけでなく build / build_stubbed もあり、データの作り方が3つあると知り、なぜ使い分ける必要があるのか疑問に思ったのがきっかけで調べました。
4.1 FactoryBot の create / build / build_stubbed
調べてみると、DB から読み直す必要がないテストなら、create ではなく build_stubbed を使うと速くなると分かりました。テストデータを作る FactoryBot には、オブジェクトの作り方が3種類ありました20。
user = create(:user) # DB に保存する
user = build(:user) # 保存しない(User.new と同じ状態)
user = build_stubbed(:user) # 保存しないが、保存済みのふりをする
DB への保存
id
向いているテスト
速さ
create
する
本物
画面表示や検索など、DB から読み直す処理
遅い
build
しない
nil
保存前のバリデーションなど
速い
build_stubbed
しない
偽の id
DB を使わないメソッド
最速
DB に触らない分だけ速いので、create が要らない場面では build / build_stubbed を選ぶ
関連も、既定では親と同じ作り方になる(build なら関連も保存されない)
build_stubbed は、DB に保存しないまま、保存済みのように振る舞います21。
user = build_stubbed(:user)
user.id # => 1001(偽の id)
user.persisted? # => true(「DB に保存済みか」を聞くと、保存済みと答える)
user.save # => RuntimeError(本当に DB に触ろうとするとエラー)
persisted? の persist は「残り続ける」という意味。「DB に保存されて、プログラムが終わっても残る状態になったか(永続化されたか)」を聞くメソッド
build_stubbed はこれに true を返すので、user_path(user) のように保存済みを前提にした URL の生成なども、DB なしでテストできる
おわりに
最後まで読んでいただき、ありがとうございました。
脚注
MDN Web Docs「<script>: スクリプト要素」 ↩︎
WHATWG「HTML Living Standard」 ↩︎
W3C「JSON-LD 1.1」 ↩︎
IETF「RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format」 ↩︎
IANA「Media Types」 ↩︎
Ecma International「ECMAScript Language Specification(ECMA-262)」 ↩︎
schema.org「About Schema.org」 ↩︎
IETF「RFC 6839: Additional Media Type Structured Syntax Suffixes」 ↩︎
Jason Miller「Islands Architecture」 ↩︎
React(旧ドキュメント)「ReactDOM」 ↩︎ ↩︎
React「hydrateRoot」「createRoot」 ↩︎
MDN Web Docs「EventTarget: addEventListener() メソッド」 ↩︎
facebook/react v17.0.2「ReactDOMComponentTree.js」「ReactDOMHostConfig.js」 ↩︎
Rails ソースコード(8-1-stable)「actionpack/lib/abstract_controller/rendering.rb」「actionview/lib/action_view/rendering.rb」「actionview/lib/action_view/base.rb」「actionpack/lib/action_controller/metal.rb」「actionview/lib/action_view/helpers/controller_helper.rb」「actionview/lib/action_view/template.rb」 ↩︎
Ruby リファレンスマニュアル「変数と定数(インスタンス変数)」 ↩︎
Ruby リファレンスマニュアル「Object#instance_variables」「Object#instance_variable_get」 ↩︎
Ruby リファレンスマニュアル「変数と定数(擬似変数)」 ↩︎
Ruby リファレンスマニュアル「Class.new」 ↩︎
Ruby リファレンスマニュアル「Object#instance_variable_set」 ↩︎
factory_bot「Build strategies」 ↩︎
thoughtbot/factory_bot v6.5.5「lib/factory_bot/strategy/stub.rb」 ↩︎
@JaeTask @yminsky yes! github.com/react/react/tree/main/compiler/package…
2 likes · 261 views
@yminsky here's one example of expect tests, we used these rigorously for react compiler: github.com/react/react/tree/main/compiler/package…
38 likes · 1 repost · 9,661 views
【警告】「npm install」しただけで感染? AI頼みの個人開発者が震えた、2026年10月のサイバー攻撃と脆弱
「えっ、プログラムを書いているだけでパソコンが乗っ取られることがあるの?」
あります。
しかも、自分が悪いプログラムを書いていなくても。
便利だからとインストールした部品に、悪意のあるプログラムが紛れ込んでいる場合があるそうです。
いや、待って。
私はAIにコードを書いてもらって、それを理解するだけでも必死なんですが。
そこにサイバー攻撃まで気を配れと?
初心者への要求スペック、高すぎない???
こんにちは。猫とAIと個人開発を愛する、でやまです。
私はエンジニアとしてはまだまだ勉強中。ChatGPTやCodexに助けてもらいながら、Webサイトを作ったり、アプリを開発したりしています。
最近は夫婦で小さなIT事務所も始めました。
つまり、ようやく「作ること」に慣れてきたくらいの人間です。
そんな私が2026年10月、Node.jsやPHPの重大な脆弱性、世界中で起きているサイバー攻撃の情報を調べてみました。
そして思いました。
これ、エンジニアだけが知っていればいい話じゃないぞ。
AIでアプリを作っている人、ホームページを運営している人、パソコンを仕事に使っている人。
今は誰でも、知らないうちに攻撃の入口を持っている可能性があります。
今回は、2026年10月25日時点で確認できた主な脆弱性と実際の攻撃を、初心者の私なりにわかりやすくまとめます。
専門用語も使います。でも、できる限り説明も付けます。
私と同じく、横文字が3つ並ぶと脳のファンが全力回転する人にも読んでいただければ幸いです。
※本記事は2026年10月25日時点の公開情報をもとに作成しています。世界中のすべての脆弱性を網羅したものではありません。また、紹介する脆弱性がすべて実際の被害につながったわけではありません。
1. そもそも「脆弱性」ってなんなの?
まずはここから。
脆弱性(ぜいじゃくせい)とは、ソフトウェアやシステムに存在するセキュリティ上の弱点のこと。
家に例えるなら、玄関には立派な鍵が付いているのに、裏口の鍵が壊れていたり、窓がきちんと閉まらなかったりする状態です。
攻撃者は、そういう弱点を探します。
しかも困ったことに、脆弱性は私たちが作ったプログラムだけに存在するわけではありません。
WindowsなどのOS
Chromeなどのブラウザ
Node.jsなどの実行環境
npmからインストールしたライブラリ
WordPressなどのWebシステム
サーバーやネットワーク機器
このような、日常的に利用しているソフトウェアにも弱点は見つかります。
つまり、私がどれだけ頑張ってコードを書いたところで、使っている部品そのものに脆弱性があれば、影響を受ける可能性があります。
私のコード、まだそこまで高度なことしてないのに、セキュリティだけ世界レベルで戦わされてる。
ちなみに、よく出てくる「CVE-2026-XXXXXX」という文字列。
これは脆弱性に付けられる識別番号です。
CVEは Common Vulnerabilities and Exposures の略。
いわば、脆弱性の管理番号ですね。
また、CVSSという危険度の評価指標もあります。
基本的には数値が高いほど深刻ですが、点数が高いからといって、すべての環境で必ず攻撃が成功するわけではありません。
そして、ここがとても大事。
「脆弱性が見つかった」と「実際に攻撃された」と「情報が盗まれた」は、全部違います。
怖い見出しを見かけたら、まずはこの3つを区別して読みたいところです。
2. 【重要】Node.jsに重大な脆弱性10件。10月21日に緊急修正
個人開発者として、まず目を疑ったのがこれ。
2026年10月21日、Node.jsの公式開発チームが10件の脆弱性に対応するセキュリティ更新を公開しました。
Node.jsとは、JavaScriptというプログラミング言語を、Webブラウザの外でも動かせるようにする仕組みです。
Webアプリを作ったり、開発ツールを動かしたりする際によく使います。
私のようにVS Codeを開いて、AIに指示を出しながらアプリを作っている人にもおなじみです。
そして私はNode.jsのバージョンなんて、普段ほとんど見ていませんでした。
動いてるからヨシ!
……よくないらしい。
今回発見された主な問題
10件の中でも特に気になるのが、CVE-2026-111497です。
これはBrotliというデータ圧縮方式を処理する部分の脆弱性。
圧縮データとは、大きなデータを小さくまとめたものです。旅行に行く前、圧縮袋で洋服を小さくするのと似ています。
ところが、攻撃者が細工したデータをNode.jsに展開させると、内部のメモリが壊れてしまう可能性があります。
結果として、プログラムの異常終了、メモリ内の情報漏えい、最悪の場合は不正なコードの実行につながるおそれがあります。
最後のものは、RCE(Remote Code Execution:リモートコード実行)と呼ばれます。
簡単にいうと、外部からサーバーに勝手な命令を実行させることです。
そのほかにも、DNSというインターネット上の住所案内に関係する問題や、通信相手の電子証明書を確認する仕組みの問題、ファイルへのアクセス制限に関係する問題が修正されています。
公式に公表された10件のCVE番号は以下です。
CVE番号
主な問題
CVE-2026-111497
Brotli圧縮データの処理によるメモリ破損
CVE-2026-105482
DNSの名前解決で別の接続先を扱ってしまう
CVE-2026-105483
特殊なIPアドレス表記による証明書検証の問題
CVE-2026-106069
IPアドレスの解釈の違いによる証明書検証の問題
CVE-2026-105481
HTTP通信で不正な応答を受け入れる
CVE-2026-106072
非標準的なIP表記による証明書検証の回避
CVE-2026-106074
特定条件で乱数やUUIDが重複・予測可能になる
CVE-2026-106070
ZIPファイルの不正な内容を誤認する
CVE-2026-106071
ファイルアクセス許可が広く認められる
CVE-2026-106073
制限されたファイルの存在や場所を調べられる
ただし、これらの問題がすべて同じ深刻度ではありません。
また、Node.jsをインストールしているだけで、必ず外部から乗っ取られるわけでもありません。
使用している機能やアプリの構成によって、実際の影響は変わります。
修正されたNode.jsのバージョン
2026年10月21日に公開された修正版はこちらです。
Node.jsの系列
修正版
22系
22.23.4
24系
24.22.1
26系
26.12.1
Node.jsにはLTSという長期サポート版があります。
新しく開発するなら、サポートされているLTSを選ぶのが基本です。ただし、既存プロジェクトは対応するNode.jsのバージョンを確認してから更新しましょう。
確認方法は簡単。
VS CodeのターミナルやPowerShellで、次を入力します。
node -v
バージョンが表示されます。
私のように「昨日まで動いていたものを更新したら、今日から動かなくなった」という悲劇を起こしたくない人は、いきなり大幅にバージョンを変えないでください。
セキュリティ対策のつもりで、自分の手でアプリを壊すのは避けたい。
公式情報:Node.js 2026年10月セキュリティリリース
3. 【実際に攻撃発生】npmの「正規パッケージ」が汚染された
正直、今回の調査で一番怖かったのがこちらです。
2026年8月、400以上のnpmパッケージを巻き込むサプライチェーン攻撃がMicrosoftによって報告されました。
サプライチェーン攻撃とは、ソフトウェアを作るために使う部品や、部品を配布する仕組みを狙う攻撃です。
npmはNode.jsで使うパッケージ管理ツール。
便利な機能を追加したいとき、たとえばこんなコマンドを使います。
npm install パッケージ名
私も使います。
というか、AIが「これをインストールしてください」と言ってくるので、言われるがまま実行してきたこともあります。
AI「こちらのコマンドを実行してください」
私「はい喜んで!」
居酒屋の店員さんより返事がいい。
でも、この攻撃を知ってからは、少し考えるようになりました。
何が起きたの?
Microsoftが2026年8月4日に報告した「ChainDrop」という攻撃では、正規のnpmパッケージの公開権限などが悪用され、悪意のあるバージョンが配布されました。
調査対象にはkeyv、flat-cache、cache-managerなどに関連するパッケージも含まれています。
攻撃者のプログラムは、インストール処理の途中で実行される仕組みを利用していました。
感染すると、開発用パソコンや自動ビルド環境に保存された認証情報を探します。
たとえば、
GitHubのトークン
npmの公開権限
クラウドサービスの認証情報
開発環境内の秘密情報
などです。
そして、盗んだ権限で別のパッケージに悪意のあるコードを混ぜ、感染を広げることもありました。
つまり、攻撃者が作った怪しい名前のパッケージをインストールするだけが危険なのではなく、普段から利用されている正規パッケージも攻撃対象になるということです。
これは困る。
私のような初心者に、パッケージの中身を全部読んで安全性を証明しろと言われても。
無理です。
猫のごはんの原材料表示ですら、全部は理解できていません。
どんな被害が考えられる?
盗まれたAPIキーで外部サービスを不正利用されたり、GitHubのソースコードを書き換えられたりする可能性があります。
最悪の場合、自分が公開するアプリに悪意のあるコードが混入することも考えられます。
APIキーとは、外部サービスを利用するための秘密の合鍵のようなものです。
これが流出すると、攻撃者が契約しているサービスを勝手に利用したり、許可されている範囲の情報を取得したりできる場合があります。
自分はアプリを作っているつもりなのに、知らない間に攻撃者のための開発環境になっていたら笑えません。
公式に確認された攻撃なので、単なる可能性の話ではありません。
ただし、npmを使った全員が感染したという意味ではありません。汚染された特定のバージョンを、影響を受ける条件で利用した場合の問題です。
Microsoftの調査:ChainDrop supply chain compromise
4. React・Next.jsも危ない? 10月14日に重大な修正
Reactは、WebサイトやWebアプリの画面を作るためによく使われるライブラリです。
Next.jsは、Reactを利用してWebアプリやWebサービスを作るためのフレームワークです。
フレームワークとは、開発に必要な仕組みをあらかじめまとめてくれた土台のこと。
例えるなら、Reactが建物を作るための部品、Next.jsは部品と建築方法がある程度セットになったもの、でしょうか。
私なら最初から完成した家を買いたいところですが、個人開発者なので今日もせっせと組み立てています。
2026年10月14日に修正された問題
React Server Componentsという、サーバー側で動作するReactの仕組みに重大な脆弱性が見つかりました。
識別番号はGHSA-xqc2-xf4c-x3h3。
条件を満たしたアプリへ特殊なデータを送ることで、外部から不正なコードを実行される可能性があります。
さらにNext.jsでも、キャッシュ機能やSVG画像の処理などで、RCEにつながる重大な脆弱性が修正されました。
修正版は次のとおりです。
製品
修正版
Next.js 15系
15.5.28
Next.js 16系
16.4.1
React Server Components 19.0系
19.0.9
React Server Components 19.1系
19.1.10
React Server Components 19.2系
19.2.9
React Server Components 19.3系
19.3.1
ただし、Reactを使っているWebサイトがすべて危険という意味ではありません。
普通のブラウザ上だけで動くReactアプリなど、今回の脆弱性に該当しない構成もあります。
まずは自分のアプリがどの仕組みを使っているのか確認することが大切です。
公式情報:Next.js 2026年10月14日セキュリティ更新
公式情報:React Server Componentsの重大な脆弱性
5. Vite・Axiosにも脆弱性。「開発中だから大丈夫」は通用しないことも
続いて、JavaScript開発者にはおなじみのViteとAxiosです。
Vite:見せてはいけないファイルが見える可能性
Viteは、JavaScriptやReactなどの開発でよく使われるツールです。
2026年10月6日と21日に、複数のセキュリティ問題が公表されました。
中でも気になるのは、開発サーバーが本来公開してはいけないファイルを返してしまう可能性がある問題です。
たとえば、.envというファイル。
これはAPIキーや設定情報などを保存するためによく利用されます。
.envは、ネット上に堂々と置いてよい秘密のメモ帳ではありません。
そもそも秘密のメモ帳を世界中の人が読めるところに置いたら、秘密でも何でもないです。
Viteの問題は、特に開発用サーバーを外部ネットワークからアクセスできる状態にしている場合に注意が必要です。
ただし、通常のローカル開発で外部からアクセスできない設定なら、同じ条件で直接攻撃される危険は抑えられます。
公式情報:Vite Security Advisories
Axios:通信に関係する問題
Axiosは、Web APIなどと通信するためによく使われるJavaScriptライブラリです。
2026年9月16日には、通信の制限を回避されたり、特殊なデータで処理が止まったりする複数の問題が公表されています。
たとえば「この通信先にはアクセスしないでね」と設定していたのに、条件によってその制限を回避されるケースです。
主な修正はAxios 1.20.0に含まれています。
もちろんAxiosを使用していなければ、Axios固有の脆弱性は直接関係ありません。
公式情報:Axios Security Advisories
6. PHP2shellって何? WordPressやホームページ管理者は要注意
2026年10月21日、PHPにも重大なセキュリティ修正が公開されました。
PHPは、WordPressなど多くのWebサービスで利用されているプログラミング言語です。
今回公表された脆弱性は、次の2件です。
CVE-2026-109618
CVE-2026-109619
この2件はPHP2shellと呼ばれています。
メモリ不足時のエラー処理や、過去の通信処理で使ったメモリの扱いに問題があり、条件によっては外部から不正なコードを実行される可能性があります。
修正版はこちらです。
PHP系列
修正版
8.2
8.2.35
8.3
8.3.36
8.4
8.4.27
8.5
8.5.12
IPA(情報処理推進機構)も10月22日に注意喚起を公表しています。
ただし、10月21日のIPA発表では、今回の脆弱性を悪用した攻撃は確認していないと説明されています。
つまり、重大な弱点が確認されたことと、実際に攻撃が起きたことは別です。
そこは冷静に読みたいところ。
そしてもう一つ。
WordPressを使っている人は、WordPress本体の更新とPHPの更新を別々に考える必要があります。
WordPressは2026年10月6日に7件のセキュリティ修正を含む7.1.3を公開しています。
PHPだけ更新しても、WordPress本体やプラグインが古いままなら別の問題が残る可能性があります。
ちなみに、私の事務所のホームページはWixで運営しています。
Wixのようなサービスでは、利用者がPHP本体を直接更新するわけではありません。サービス提供者が管理する部分と、私たちが管理する部分を分けて考える必要があります。
何でもかんでも「PHPを更新しなきゃ!」と焦る必要はありません。
まず自分が何を使っているかを確認する。それが大事です。
公式情報:IPA:PHP2shellの脆弱性対策
公式情報:WordPress 7.1.3セキュリティ更新
7. Windows・Chromeも他人事じゃない
ここまで読んで、
「私はプログラミングなんてしないし、関係ないかな」
と思った人。
まだ帰らないでください。
WindowsやChromeにもセキュリティ更新が必要です。
Windows 11
Microsoftは2026年10月、複数の脆弱性を修正しました。
特にCVE-2026-96951については、実際の悪用が確認されています。
Windowsの安全機能を回避される可能性がある問題で、IPAも10月14日に緊急の注意喚起を出しています。
対応は基本的にWindows Updateです。
「設定」からWindows Updateを開き、最新の更新を確認しましょう。
更新後に再起動が必要な場合もあります。
私は再起動を後回しにしがちです。
タブを大量に開いて、何を調べていたのか自分でも覚えていないからです。
しかし、セキュリティ更新の再起動は後回しにしない方がよさそうです。
Google Chrome
Googleも2026年10月20日にChrome 156を公開しました。
この更新には、353件のセキュリティ修正が含まれています。
353件。
私の未処理タスクより多い気がします。
Chromeは「設定」→「Chromeについて」から更新を確認できます。
Chromeだけでなく、Microsoft EdgeやFirefoxなど、普段使うブラウザも更新しましょう。
公式情報:IPA:2026年10月のMicrosoft製品の脆弱性
公式情報:Google Chrome 2026年10月20日更新
8. 【スマホアプリ開発者は特に注意】アプリを解析して裏側のサーバーを攻撃する「A2B攻撃」
これは個人的に、かなり気になったニュースです。
2026年10月13日、情報通信研究機構(NICT)が、公開Androidアプリを解析してサーバーを攻撃する手法について注意喚起を公表しました。
名前はA2B攻撃(App-to-Backend Attack)。
Appはアプリ、Backendはアプリの裏側で動くサーバーのこと。
たとえば、スマホアプリで会員登録するとき、入力した情報を保存するサーバーがありますよね。
攻撃者は公開されているアプリを調べて、裏側でどんな通信をしているのか、どんなサーバーにつながっているのかを分析します。
そして、その情報をもとにサーバーの弱点を狙います。
表側のアプリを調べて、裏側の秘密を探ろうとするわけです。
私はまだアプリをストアで公開する準備をしている段階ですが、これは覚えておくべきだと思いました。
アプリの中に秘密のAPIキーを直接入れてしまったり、サーバー側のアクセス制限を適切に設定していなかったりすると、危険が大きくなる場合があります。
大事なのは、アプリ内に書いた情報は解析される可能性があるという前提で設計すること。
NICTは、同様の問題がiOSアプリやWebサービスでも起こり得ると説明しています。
AIのおかげでアプリを作るハードルは下がりました。
でも、アプリを公開するなら、利用者の情報を守る責任も付いてきます。
AIに「アプリ作って!」と頼むだけでは、セキュリティ対策まで自動的に完璧になるわけではない。
耳が痛いですね。
公式情報:NICT:アプリ解析からのサーバ攻撃への注意喚起
9. 実際に企業が攻撃されたら、何が起きるの?
ここからは仮定の話ではありません。
2026年に企業が公表した、実際の被害について紹介します。
NEC:委託先の攻撃から顧客システムにも被害
2026年10月20日、NECは委託先へのサイバー攻撃によって、顧客システムへの不正アクセスや情報漏えいが発生したと公表しました。
委託先の正規アカウントの認証情報が盗まれ、それを使って顧客システムへアクセスされたとのこと。
結果として、ファイルの暗号化、システム停止、個人情報やシステム関連情報の漏えいが確認されています。
ここで怖いのは、自分の会社が直接攻撃されなくても、仕事を任せている相手から被害が広がる可能性があることです。
私は小さなIT事務所を運営しています。
将来、お客様のホームページやアカウントをお預かりする機会もあります。
つまり、私の管理が甘かった場合、私だけでなくお客様にも迷惑をかける可能性がある。
これは本当に気を付けたいところです。
公式情報:NEC:委託先への不正アクセスに起因する被害について
タイムズカー:本人確認書類の画像データにも影響
2026年10月23日、タイムズモビリティはWebシステムへの不正アクセスに関する調査結果を公表しました。
その中では、本人確認書類の画像データ約146万件について、外部送信の痕跡が確認されたと説明されています。
ただし、画像データは暗号化された状態で保管されていたため、外部送信の痕跡があることと、画像の中身をそのまま閲覧されたことは同じではありません。
この区別は大切です。
また、同社は被害を受けたあと、本人確認書類の画像の保有方法についても見直しています。
この事件から私が学んだのは、必要以上の個人情報を長期間保存すること自体が、事業者にとってリスクになるということです。
これは、小さなWebサービスでも無関係ではありません。
公式情報:タイムズモビリティ:不正アクセス調査結果
PingCAP:GitHubの認証情報が侵入経路の一部に
2026年10月23日、データベース技術を提供するPingCAPもセキュリティ事故について公表しました。
侵害されたGitHubアカウントや露出していた認証情報を利用され、社内システムへの不正アクセスや情報のコピーが行われたと説明しています。
開発者としては、かなり気になる事件です。
GitHubはソースコードを保存するだけでなく、自動ビルドや公開作業にも使えます。
つまり、GitHubの認証情報が漏れると、被害がソースコードの閲覧だけで済まない場合があります。
GitHubのパスワードを適当にしていたら、未来の自分が泣く。
そういうことですね。
公式情報:PingCAP Trust Hub
10. ハッカーはどうやって攻撃してくるの?
ここまで読むと、攻撃者というのは映画に出てくるような、黒いパーカーを着た天才プログラマーばかりだと思うかもしれません。
でも、現実の攻撃はもっと地味で、もっと厄介です。
たとえば、次のような方法があります。
フィッシング
本物の会社やサービスを装ったメール・SMS・Webサイトなどで、パスワードや認証情報を盗み取る手口です。
認証情報の悪用
盗まれたIDやパスワードを使って、正規の利用者としてログインします。
脆弱性の悪用
更新されていないソフトウェアの弱点を使い、サーバーやネットワークへ侵入します。
サプライチェーン攻撃
正規のソフトウェアやライブラリの配布経路に悪意のあるコードを紛れ込ませます。
そして、被害を受けた企業に金銭を要求する攻撃もあります。
ランサムウェアと二重脅迫
ランサムウェアとは、データを暗号化して使用できなくするなどして、身代金を要求するマルウェアの一種です。
さらに近年は、データを盗んだうえで、
「お金を払わなければ公開する」
と脅す手口もあります。
これを二重脅迫(ダブルエクストーション)と呼びます。
2026年10月、米国CISAとFBIなどは、ランサムウェア集団SafePayについて共同注意喚起を公表しました。
2024年8月以降、少なくとも600組織に影響があったと報告されています。
この集団は、認証情報の窃取、VPNの悪用、未修正の脆弱性を使った侵入などを行い、データの窃取や暗号化を通じて金銭を要求する手口が確認されています。
怖いですね。
でも、これを知ったからといって、パソコンを窓から投げ捨てるわけにもいきません。
まだローンの残っているパソコンだったら、物理的な被害が増えるだけです。
私たちにできるのは、攻撃の入口を減らして、万が一の被害を小さくすることです。
参考:CISA・FBIなどによるSafePayへの共同注意喚起
11. AI頼みの個人開発者が、今日からできること
ここまで読んで、
「わかった。危ないのはわかった。で、何をすればいいの?」
となった人。
大丈夫です。
私も同じです。
専門用語を50個覚えるより、まずは実際に使っているパソコンや開発環境を確認しましょう。
① Windowsとブラウザを更新する
まずは一番身近なところから。
Windows Updateを確認し、ChromeやEdge、Firefoxなども更新します。
必要な再起動も忘れずに。
② Node.jsとnpmのバージョンを確認する
VS CodeのターミナルやPowerShellで、次を実行します。
node -v
npm -v
使用しているバージョンが表示されます。
Node.jsは公式のセキュリティ情報と照らし合わせて、修正が必要か確認しましょう。
ただし、プロジェクトが必要とするバージョンを調べずに、いきなり最新版へ変更するのは避けたいところです。
③ npmの脆弱性を確認する
開発中のプロジェクトフォルダで、次のコマンドを実行します。
npm audit
既知の脆弱性を持つ依存パッケージがあるか確認できます。
さらに、
npm outdated
で更新候補を調べられます。
ただし、npm auditで問題が表示されなくても、未知のマルウェアまで完全に見つけられるわけではありません。
また、何も考えずにnpm audit fix --forceを実行すると、互換性のないバージョンへ更新される場合があります。
セキュリティ対策でアプリが起動しなくなったら、それはそれで泣きます。
まずは結果を確認し、必要な修正だけをテストしながら行いましょう。
④ GitHubのセキュリティ設定を確認する
GitHubには、依存パッケージの脆弱性を知らせるDependabot alertsや、秘密情報の漏えいを検出するSecret scanningなどがあります。
利用できる機能は、リポジトリやプランなどの条件によって異なります。
GitHubのアカウントには多要素認証を設定し、不要なアクセストークンや、必要以上の権限を持つトークンがないか確認しましょう。
⑤ APIキーをソースコードに直接書かない
AIを使っていると、コードが動くことを優先してしまいがちです。
しかし、公開するソースコードやブラウザから読み取れるプログラムに、秘密のAPIキーを直接埋め込むのは危険です。
.envや各サービスの秘密情報管理機能などを利用して、用途に合った場所で管理することが大切です。
なお、.envなら何でも安全というわけではありません。公開環境への配置方法やアクセス制限も重要です。
すでに秘密情報を公開してしまった場合は、ファイルから削除するだけでなく、その認証情報を無効化・再発行することも必要です。
一度流出した合鍵は、隠し直しても合鍵のままです。
⑥ AIが提案したコマンドをそのまま実行しない
これが私には一番刺さります。
AIが出してくれたコードやコマンドはとても便利です。
でも、AIが提案した内容が常に安全とは限りません。
特に、新しいパッケージのインストール、ファイル削除、権限変更、外部サイトからのスクリプト取得などは、実行前に内容を確認するようにしましょう。
わからなければ、別のAIに「このコマンドは何をする? 危険なところはない?」と確認する方法もあります。
ただし、AI同士が同じ間違いをすることもあるので、重要な変更は公式ドキュメントと照らし合わせる必要があります。
AI「安全です」
別のAI「安全です」
私「じゃあ安全……なのか?」
多数決で決める話ではありません。
⑦ バックアップを取る
大事なソースコード、設定、データは定期的にバックアップしましょう。
GitHubへpushしているだけでは、すべてのバックアップが十分とは限りません。
データベースや、GitHubに保存していない設定、公開サービスのデータなども確認が必要です。
特に、バックアップ先まで同じアカウントで削除できる構成だと、アカウント乗っ取りによって一緒に消される危険があります。
別の場所にも保管し、必要なときに復元できるか確認することが大切です。
12. 「でも私、Wixでホームページ作ってるだけだよ?」という人へ
Wixのようなホームページ作成サービスを使っている人は、Node.jsやPHPの更新を自分で実施する必要があるとは限りません。
基盤部分はサービス提供者が管理しているためです。
ただし、それで完全に安全という意味ではありません。
たとえば、
Wixアカウントの乗っ取り
不要な共同管理者の放置
外部サービスとの連携設定
問い合わせフォームで扱う個人情報の管理
偽メールによる認証情報の窃取
などは、サイト運営者自身にも関係する問題です。
私もWixでホームページを運営しているので、今回の調査を機に管理者や連携アプリの設定を確認しようと思いました。
サーバーの難しい設定を自分で管理しなくてよいのはありがたいことです。
しかし、管理しているアカウントそのものを盗まれたら大変です。
「Wixだから安全」「WordPressだから危険」といった単純な話ではありません。
どのサービスにも、サービス提供者が守る部分と、利用者が守る部分があります。
そこを理解しておきたいですね。
13. そもそも、私たち初心者はどうやって勉強すればいいの?
ここまで読んで、少し疲れませんでしたか?
私は疲れました。
脆弱性、CVE、RCE、npm、TLS、DNS、CI/CD、APIキー、サプライチェーン……。
もうアルファベットはお腹いっぱいです。
でも、今回調べていて気付いたことがあります。
全部の専門用語を暗記する必要はない。自分が使っているものを把握することが、最初の一歩になる。
自分のパソコンはWindowsなのかLinuxなのか。
自分のアプリはNode.jsを使っているのか。
Next.jsなのか、普通のJavaScriptなのか。
npmから何をインストールしているのか。
GitHubの認証情報はどこで管理しているのか。
そういう基本情報を一つずつ知っていくことが大切なんだと思います。
そして、これはAIを使って開発している人ほど意識したいことです。
AIは、コードを書いたり、エラーを調べたり、わからない言葉を説明したりしてくれます。
でも、公開するアプリに責任を持つのは、最終的には開発者です。
私は「AIが作ったからわかりません」で済ませないようにしたい。
もちろん、まだまだわからないことだらけです。
それでも、「これは何のために必要なの?」「どうしてこのライブラリを使うの?」「このコードに危険はないの?」と確認する習慣は付けられます。
完成させることと、安全に使ってもらうこと。
個人開発者として、両方を目標にしたいと思いました。
14. 最後に。怖がるより、確認しよう。
今回の調査をして、正直少し怖くなりました。
自分が一生懸命作ったアプリ。
時間をかけて整えたホームページ。
AIに何度も「そこじゃない」と言いながら修正してもらったコード。
それらが攻撃で壊されたり、利用者に迷惑をかける原因になったりするのは避けたいものです。
でも、怖いからといって開発をやめる必要はありません。
セキュリティの脆弱性は、発見され、修正され、対策が進められています。
大切なのは、「何が危険なのか」「自分が使っているものに関係するのか」「何を更新すればよいのか」を知ること。
私のような初心者でも、まずはバージョンを確認するところから始められます。
そして、全部を一日で完璧にする必要もありません。
体調のいい日に一つずつ。
今日できなければ、別の日に。
ただし、実際に悪用されている重大な脆弱性が自分の公開システムに該当する場合は、放置せず、必要に応じて公開停止や管理者への相談なども含めて早急に対応したいところです。
AIにコードを書いてもらう時代だからこそ、そのコードを使う私たちにも最低限のセキュリティ知識が必要なのだと思います。
ちなみに、この記事を書いている私は、セキュリティの専門家ではありません。
AIと一緒に個人開発をしている、まだまだ勉強中の人間です。
だからこそ、同じ初心者の方に、難しい言葉をできるだけかみ砕いて伝えたいと思い、今回の記事を書きました。
最後にひとつだけ。
「昨日まで動いていたから、今日も安全」とは限りません。
私自身、これを忘れないようにしたいと思います。
さて、まずは自分のNode.jsのバージョンでも確認してきます。
もし古かったら?
……そのときはAIに相談しながら、慎重に更新します。
AI頼みは続くんかい。
続きます。
でも、丸投げは少しずつ卒業したい。
そんな個人開発者のセキュリティ勉強記録でした。
最後まで読んでいただき、ありがとうございました。
参考資料・一次情報
この記事は2026年10月25日時点で公開されていた各開発元、政府・公的機関、被害企業の情報をもとに作成しました。
開発環境・ソフトウェア
Node.js公式:2026年10月のセキュリティリリース
React公式:React Server Componentsの重大な脆弱性
Next.js公式:2026年10月のセキュリティ更新
Vite公式:セキュリティ情報一覧
Axios公式:セキュリティ情報一覧
WordPress公式:7.1.3セキュリティリリース
Google Chrome公式:2026年10月20日更新
国内の公的機関
IPA:PHP2shellの脆弱性対策
IPA:2026年10月のMicrosoft製品の脆弱性対策
IPA:重要なセキュリティ情報一覧
NICT:アプリ解析からのサーバ攻撃への注意喚起
実際の攻撃・企業被害
Microsoft Security:ChainDropサプライチェーン攻撃
NEC:委託先への不正アクセスに起因する被害
タイムズモビリティ:不正アクセス調査報告
PingCAP:Trust Hub
CISA・FBIなど:SafePayランサムウェアへの注意喚起
※実際の被害については、各社が公表した範囲を記載しています。未確認の攻撃原因や被害規模を断定するものではありません。
#サイバーセキュリティ #個人開発 #生成AI #プログラミング初心者 #Nodejs
Mentions
One API Key, One Balance: Wiring Models and Data APIs into Claude Code, Cursor and Codex Disclosure: this post is written by the OpenAny team. OpenAny is our product. We tried to keep it practical, and the limitations section is there on purpose. The problem: every new tool is another key and another bill A coding agent rarely stops at one model. In a typical week ours used a frontier model for planning, a cheap fast model for bulk edits, a search API to read the docs, and something to pull GitHub repository metadata. Every one of those meant: - a new account and a new API key in .env, - another prepaid balance or card on file, - another usage dashboard to check when a bill looked wrong, - another set of rate limits and error formats for the agent to learn. Agents make this worse because they decide to call things on their own. "Which key paid for that call, and how much did it cost?" gets hard to answer quickly. We built OpenAny for that case. It's a pay-per-call marketplace where models, data APIs and agent skills sit behind one API key and one prepaid balance. This post shows the actual setup in Claude Code, Cursor and Codex, the commands involved, and where it isn't a good fit. What you get (and what you don't) - Models through an OpenAI-compatible Chat Completions endpoint at https://api.openany.si/v1. - Data and tool APIs (search, GitHub, finance, e-commerce, SEO and more), called through the openany CLI. Each listing shows a fixed price and a billing unit. - Skills: portable SKILL.md workflows. They run inside your agent and call the models and APIs above. OpenAny doesn't host a skill runtime. - Billing: one shared prepaid balance with no subscription. You can top up by card, USDC or x402, and card auto top-up and USDC subscriptions are both available. New accounts get $1 in signup credit that doesn't expire. As of 2026-11-03 the published catalog lists 154 models, 3,138 API endpoints and 59 provider identities. Those are listing counts, not uptime claims. There's no MCP server. The integration is a CLI, a skill file and plain HTTP endpoints. Step 1: Get a key and make a first call Create an account, then create an API key from the dashboard. Put the key in the environment of the process that runs your agent, not in a prompt: export OPENANY_API_KEY="..." # better: your shell's secret manager or the client's secret store Pick a model ID from the model catalog. Model IDs are route-specific, so copy the ID from the listing instead of guessing. The examples below use deepseek-v4-flash, which was listed when we wrote this. cURL: curl --fail-with-body --silent --show-error \ https://api.openany.si/v1/chat/completions \ -H "Authorization: Bearer $OPENANY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "deepseek-v4-flash", "messages": [{"role": "user", "content": "Reply with exactly: OpenAny connected"}], "max_tokens": 32 }' Python (official openai package): import os from openai import OpenAI client = OpenAI( api_key=os.environ["OPENANY_API_KEY"], base_url="https://api.openany.si/v1", ) response = client.chat.completions.create( model=os.environ.get("OPENANY_MODEL", "deepseek-v4-flash"), messages=[{"role": "user", "content": "Reply with exactly: OpenAny connected"}], max_tokens=32, ) print(response.choices[0].message.content) print(response.usage) A successful call returns HTTP 2xx with a non-empty choices[0].message.content. The usage block tells you what you were billed for. Some models have more than one provider route. The catalog listing shows the routes for each model. Step 2: Install the CLI and the skill Data APIs and skills go through the openany CLI (Node.js 22 or later): npm install -g openany-cli@latest --registry=https://registry.npmjs.org/ openany --version openany setup --scenario call # paste the key at the hidden prompt, never in chat openany status --scenario call -j openany balance -j setup saves the key to ~/.config/openany/credentials.json with 0600 permissions. If OPENANY_API_KEY is set, it takes precedence over the saved key. Next, install the OpenAny skill where your agent looks for skills. The file is the same for every client. Only the directory changes: | Client | Skill location | |---|---| | Claude Code | .claude/skills/openany/SKILL.md | | Cursor | .cursor/skills/openany/SKILL.md | | Codex | .agents/skills/openany/SKILL.md | # Example for Claude Code; swap the directory for Cursor or Codex mkdir -p .claude/skills/openany curl -fsSL https://openany.si/SKILL.md -o .claude/skills/openany/SKILL.md Read the file before you install it. It's plain Markdown, and you should know what instructions you're handing to your agent. There's also a shortcut: paste Read openany.si/AGENT.md and connect OpenAny into the agent and let it walk through the same steps. None of this replaces your agent's main model. Claude Code still uses whatever model you already run. The skill only lets the agent call external models and APIs through the CLI when a task needs them. Step 3: Call a data API from your agent Say your agent needs repository metadata such as stars, forks, language and license. Here's what the flow looks like with the CLI. Catalog browsing doesn't need a key: openany discover -q "github repository" -l 5 --public -j openany inspect --id 2a01d388-02fb-4307-8e47-f3a814d9a3cd --public -j inspect returns the method, the input schema and the price. For this endpoint, the input is a required url in the request body, and the billing unit is a fixed price per call ($0.0012 when we checked). Then run it: openany run --id 2a01d388-02fb-4307-8e47-f3a814d9a3cd \ -i '{"url": "https://github.com/facebook/react"}' \ --query '{"max_cost_usd": "0.01"}' -j The response is the repository metadata as JSON, plus the request ID and the amount charged. That max_cost_usd parameter caps the price for this one request. If no route can serve the call at or below that price, the request is refused before it runs and nothing is charged. The same endpoint accepts fields, max_items and jq query parameters, which shrink the response without changing the cost. That's useful when the agent doesn't need 40 KB of JSON back. In practice you don't type these commands yourself. You give the agent a bounded task like this: > Use the openany skill. Find an API that returns GitHub repository metadata, show me the price before calling, then fetch facebook/react. Budget: $0.05. The skill tells the agent to check the balance, discover the API, inspect it, show the cost and ask before spending. After the call it should report the request ID and the charge. To check afterwards, run openany calls list --limit 20 -j or openany calls get --id REQUEST_ID -j. Step 4 (optional): Use OpenAny as the main model provider This is separate from tool access. Only do it if you actually want your editor's chat model billed through OpenAny. Cursor. In Cursor Settings → Models, enter your OpenAny key in the OpenAI API key field. If your version shows Override OpenAI Base URL, set it to https://api.openany.si/v1. Then add an exact model ID from the catalog and send one short test message. Two caveats: Cursor limits own-key access to supported chat models, and Tab completion still uses Cursor's built-in models. Codex. Merge a custom provider into your user-level ~/.codex/config.toml: model = "MODEL_ID_FROM_LISTING" model_provider = "openany" [model_providers.openany] name = "OpenAny" base_url = "https://api.openany.si/v1" env_key = "OPENANY_API_KEY" wire_api = "responses" Codex uses the Responses API, so the model you choose needs a route that supports Responses, streaming and tool calls. Chat Completions support alone doesn't guarantee that. Restart Codex, send one small request, and check the Usage page. Check the model listing for Responses support before you switch. Claude Code. Our documented Claude Code setup is the skill and CLI from Step 2, with Claude Code's own model left unchanged. Cost control with a prepaid balance Here's what actually keeps spending bounded: 1. It's prepaid. The worst case is your balance, not an open-ended invoice. There's no subscription, and the $1 signup credit is enough to test the setup. 2. Prices are visible before the call. Every API listing has a fixed price and a billing unit, and inspect returns them, so the agent can show the cost before spending. 3. There's a per-request cap. max_cost_usd on data APIs refuses an over-budget call instead of charging for it. 4. Usage is recorded per call. openany calls list shows a chargedCostMicroUsd for each row, and the dashboard has a usage page. 5. Auto top-up is opt-in. Card auto top-up and USDC subscriptions each need their own authorization. If you let an agent top up through the CLI, you set the network, the per-payment limit, the total limit and an expiry up front. The pricing page explains billing units and the difference between balance-funded calls and direct x402. Limitations: when not to use this - If you only use one provider. If everything you run is on one vendor's API and their billing works for you, an extra layer only adds a hop. - If you need a hosted MCP server. OpenAny doesn't publish one. The integration is the CLI, SKILL.md and HTTP. - If your agent can't run a terminal. Data APIs and skills go through the Node CLI. An HTTP-only client can still use the model endpoint, but not the CLI workflow. - If you need every protocol on every model. Chat Completions works across the catalog, but Responses and Anthropic Messages support depend on the route. Check before switching a main model. - If you expect x402 everywhere. x402 funds your balance. Paying per request directly with x402 only works on endpoints that explicitly support it. - If you need a long track record. We launched in October 2026. Listing counts aren't uptime guarantees, so try a few calls before building anything critical on top. Wrapping up The practical win is boring, and that's the point. Your agent gets one key, one balance and one usage log, and the main model stays the same unless you choose to change it. Setup is a CLI install, a skill file and an environment variable. If you try it, the docs have the full client recipes. We'd like to hear where it breaks, especially Cursor and Codex edge cases. You can reach us at team@openany.si. If you run an API and want agents to be able to call it, you can list it on OpenAny for free and set your own prices. Payment and payout fees may still apply.
Here was the bug I eventually traced it to react#26048
React has no dependencies: npmjs.com github.com/react/react/blob/main/package.json If you read the first comment on your link, you'd see why you're wrong: > That's not _React_, that's `create-react-app`
Not even a link to a gh issue about the root cause? 2 secs Google search: react#31819
Google Translate was crashing our React app — here's how we fixed "removeChild" for good --- title: Google Translate was crashing our React app — here's how we fixed "removeChild" for good published: true description: Our most engaged user hit "Failed to execute 'removeChild' on 'Node'" because Chrome was auto-translating the page. Why it happens, how to reproduce it without Google Translate, and the three-layer fix we shipped. tags: react, javascript, webdev, debugging cover_image: dev-to-uploads.s3.us-east-2.amazonaws.com --- I build QRflows, a dynamic QR code platform. Last week I was going through session recordings and found our most engaged user of the month: someone in Hong Kong who spent 11 minutes in the dashboard, clicked 82 times, tried half a dozen QR types, added a logo, built a landing page. At minute three, the page crashed and reloaded. The error in the console: NotFoundError: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node. The recording made the cause obvious once I noticed it: the whole dashboard was in Chinese. Our site only exists in English. The user had Chrome's built-in Google Translate turned on. This is one of those bugs that never shows up in your own testing, because you never translate your own app. Here's what's going on, how to reproduce it without Google Translate, and the three-layer fix we shipped. --- Why Google Translate breaks React React keeps references to the DOM nodes it created, including plain text nodes. When state changes, it updates or removes those exact nodes. Google Translate doesn't know about React. When it translates a page, it walks the DOM and replaces text nodes with its own <font> elements containing the translated text: <!-- What React rendered --> <button>Save</button> <!-- What the DOM looks like after Google Translate --> <button><font><font>保存</font></font></button> The original "Save" text node is gone from the DOM, but React still holds a reference to it. The next time React needs to remove or replace that text, it calls parent.removeChild(textNode) — and the browser throws, because that node is no longer a child of anything. The same thing happens with insertBefore when React tries to insert something next to a node that Translate has swapped out. It's a long-standing issue — facebook/react#11538 has been open since 2017 — and it isn't specific to React: anything that holds references to text nodes is vulnerable. If you want the deep dive, Martijn Hols' write-up is excellent. Which code is vulnerable The crash needs two things: a bare text node and something conditional next to it. Patterns like these: // Text sitting next to a conditional element <button> {isSaving && <Spinner />} Save </button> // Conditional text {error && error.message} // Ternary returning strings <span>{isSaving ? "Saving…" : "Saved"}</span> // Text mixed with an expression <p>Uploaded {count} files</p> Every QR builder has dozens of these: status labels, counters, "Uploading… / Done", plan badges. Which is why the crash hit our most active user and nobody else. --- Reproducing it without Google Translate Waiting for a translated session to show up in your logs is not a test plan. You can simulate what Translate does to the DOM in the browser console: function fakeTranslate(root = document.body) { const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { acceptNode: (node) => node.nodeValue.trim() && node.parentElement && !node.parentElement.closest('script, style, [translate="no"], font') ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT, }); const nodes = []; while (walker.nextNode()) nodes.push(walker.currentNode); for (const textNode of nodes) { const outer = document.createElement('font'); const inner = document.createElement('font'); inner.textContent = '译' + textNode.nodeValue; // mark it as "translated" outer.appendChild(inner); textNode.parentNode.replaceChild(outer, textNode); } return nodes.length; } window.addEventListener('error', (e) => console.log('CRASH:', e.message)); fakeTranslate(); Paste it, run fakeTranslate(), then use the app: switch tabs, toggle options, type into inputs, navigate between routes. Run fakeTranslate() again after each step, because real Translate keeps re-translating new content as it appears. On our dashboard, switching QR types after this reproduced the exact crash from the recording within a few clicks. --- The fix: three layers There's no single clean fix. What worked for us is three layers, from "never crash" to "fewer weird states". Layer 1: a DOM guard so the crash can't happen This is the workaround from the React issue thread: patch removeChild and insertBefore so that, instead of throwing, they skip the operation when the node relationship is no longer what React expects. // src/lib/domTranslateGuard.ts export function installDomTranslateGuard(): void { if (typeof window === 'undefined' || typeof Node !== 'function') return; const proto = Node.prototype as any; if (proto.__translateGuardInstalled) return; proto.__translateGuardInstalled = true; const originalRemoveChild = proto.removeChild; proto.removeChild = function (child: Node) { if (child.parentNode !== this) { console.warn('[dom-guard] removeChild skipped', child, this); (window as any).clarity?.('event', 'dom_guard_removeChild'); return child; } return originalRemoveChild.call(this, child); }; const originalInsertBefore = proto.insertBefore; proto.insertBefore = function (newNode: Node, referenceNode: Node | null) { if (referenceNode && referenceNode.parentNode !== this) { console.warn('[dom-guard] insertBefore skipped', referenceNode, this); (window as any).clarity?.('event', 'dom_guard_insertBefore'); return newNode; } return originalInsertBefore.call(this, newNode, referenceNode); }; } Two details that matter: Install it before React touches the DOM. We call it as the very first thing in the client entry, before hydrateRoot: // entry-client.tsx import { installDomTranslateGuard } from './lib/domTranslateGuard'; installDomTranslateGuard(); // ...then hydrateRoot(...) Keep it out of SSR. It only lives in the client entry, and the typeof window check is a second line of defence. The server never touches Node.prototype. The trade-off is honest: this hides the error instead of fixing the cause. A skipped insertBefore can occasionally leave a piece of UI missing until the next render. That's why we log it and send a custom event to our session analytics — so we can see how often the guard actually fires, and on which pages. Layer 2: stop giving Translate bare text nodes The guard prevents crashes. Wrapping text in elements prevents most of the situations where it would have to step in. If text lives inside its own <span>, Translate replaces the text inside the span, and React only ever adds or removes the span itself — which is still there. // Before <button> {isSaving && <Spinner />} Save </button> // After <button> {isSaving && <Spinner />} <span>Save</span> </button> // Before {error && error.message} // After {error && <span>{error.message}</span>} // Before <p>Uploaded {count} files</p> // After <p><span>Uploaded {count} files</span></p> No visual change, no new classes. We went through every component in the QR builder, the file upload flow, the editor and the download modals, and wrapped anything that changes at runtime: status labels, counters, button text next to spinners. Layer 3: translate="no" — but only where it makes sense The tempting shortcut is <html translate="no">. Don't. Your users turned on translation because they need it — breaking the crash by breaking the translation just moves the problem. We only opt out content that shouldn't be translated anyway: - URL, email, phone and password inputs - short links and destination URLs - the QR preview and technical labels (sizes, PNG/SVG) - the brand name in the header and footer <input type="url" translate="no" /> <span translate="no">{shortUrl}</span> Everything else — headings, buttons, help text — still gets translated. --- How we verified it After deploying, I ran the fake-translate script on the live site: the public PDF QR generator, then the logged-in dashboard. I switched QR types back and forth, toggled the logo option, typed into inputs, and navigated from the builder to the landing page editor and back — re-running fakeTranslate() after each step. Result: zero uncaught errors, no reloads, and the UI kept updating live with the "translated" text in place. The brand name and URL fields stayed untouched, as intended. We'll keep watching the dom_guard_* events in session analytics. If they show up a lot on a specific page, that page still has bare text nodes to wrap. --- TL;DR - Google Translate replaces text nodes with <font> elements. React still holds the old nodes and crashes on removeChild / insertBefore. - You can reproduce it without Translate by wrapping text nodes in <font> from the console. - Layer 1: patch removeChild / insertBefore before hydration, client-only, and log when it fires. - Layer 2: wrap conditional and dynamic text in <span>. - Layer 3: translate="no" only on inputs, URLs and brand names — never on the whole app. The part that stuck with me: this bug hit the single most engaged user we had that month. International users who translate your app are often exactly the people working hardest to use it. --- I'm building QRflows in public — dynamic QR codes with Smart Rules, real-time analytics and a Claude MCP integration. If you've hit this bug with a different framework, I'd like to hear how you handled it.
@rickyfm @acemarke @infinterenders medium.com Wrote about it a while back, useState seems to call the basicStateReducer github.com/react/react/blob/v19.2.0/packages/reac…
新卒エンジニアが書き溜めた技術メモ 4選 はじめに こんにちは、namonaki です。新卒で入社して半年、分からなかったことを個人用のメモに書き留めてきました。その中からいくつか選りすぐって、改めて調べつつ記事にしてみました。 ■ 対象 Rails や React でコードは書けるが、「なぜこう書くと動くのか」はまだ説明できない人 普段なんとなく使っている仕組みの裏側を、ソースコードや DevTools で確かめてみたい人 ■ 扱うこと ブラウザ・Web標準 React と Islands Architecture Rails テスト 各章は独立しているので、気になるところから読んでください。参考にした資料は、本文中の番号から末尾の脚注に飛べます。 ! 前提のバージョン(2026年10月時点) Ruby 4.0 / Rails 8.1 React 17 / Node.js 24 factory_bot 6.5 1. ブラウザ・Web標準 JSON-LD(検索エンジン向けの構造化データ)について調べていたとき、JS を書くわけでもないのに、なぜ <script> タグを使うのか疑問に思ったのがきっかけです。 1.1 <script> の属性なし / async / defer の違い 調べてみると、HTML の要素を探して操作する JS には、defer を付けるのが基本でした。ダウンロードは早く始めつつ、実行は HTML の解析が終わるまで待ってくれるからです。 <script src="a.js"></script> <!-- ① 属性なし --> <script src="a.js" async></script> <!-- ② async --> <script src="a.js" defer></script> <!-- ③ defer --> HTMLの解析を止めるか 実行タイミング 実行順 属性なし 止める 読み込んだらすぐ 書いた順 async ダウンロード中は止めない ダウンロードが終わりしだい 保証なし defer 止めない HTMLの解析が終わった後 書いた順 defer は「ダウンロードは早く、実行は最後」という良いとこ取り1 念のため、コード側でも DOMContentLoaded を待つと二重の保険になる querySelectorAll のように要素が見つからなくてもエラーにならない書き方では、何も起きないだけなので気づきにくい。defer を付けておけば、この「要素がまだ無い」事故を防げる 1.2 Webのルールは誰が決めているのか 調べてみると、Web のルールは1つの団体が決めているわけではなく、部分ごとに別の団体が決めていました。 パンくずリストを構造化データで書いたときの例で見てみます。 <script type="application/ld+json"> { "@context": "schema.org", "@type": "BreadcrumbList", "itemListElement": [ ... ] } </script> この数行でも、部分ごとにルールを決めている団体が違いました。 部分 ルール 決めている団体 <script> タグ HTML WHATWG application/ld+json MIMEタイプの仕組み / 登録 IETF / IANA { ... } の書き方 JSON IETF・Ecma International @context などの書き方 JSON-LD W3C BreadcrumbList という単語 語彙 schema.org WHATWG:HTML と DOM。ブラウザを作る企業が中心。2019年に W3C と合意し、HTML は「HTML Living Standard」に一本化。版の区切りはなく更新され続ける2 W3C:CSS、アクセシビリティ(WAI-ARIA)、JSON-LD など3 IETF:HTTP や JSON など、通信とデータ形式の基礎(RFC)4 IANA:MIMEタイプの一覧、ドメイン名、IPアドレスの登録・管理5 Ecma International:JavaScript の文法(ECMAScript)6 schema.org:Google・Microsoft・Yahoo! などによる共同プロジェクト。標準化団体ではなく、構造化データ用の単語帳を作っている7 ■ 補足 +json は「中身は JSON 形式」という意味の接尾辞8 <script> の type に JS 以外を指定すると、ブラウザは実行せずデータとして置いておく。検索エンジンはこれを読む 2. React と Islands Architecture Wikipedia のような辞書的なページは、全体を SPA にするとかえって重くなるので、一部分だけを React で動くようにすることがあると思います。こうした構成にも何か名前が付いているはずだと思い、調べてみたのがきっかけです。 2.1 「一部だけReact」すなわち Islands Architecture …ではなかった 最初は、一部だけ React を使う構成ならすべて Islands Architecture だと思っていましたが、サーバーが空の div を返し、ブラウザで React が中身を一から作る構成は、Islands Architecture ではありませんでした。Islands では島の HTML もサーバーが作り、ブラウザはそれを hydrate するからです。 ■ Islands Architecture の条件9 島(インタラクティブな部分)の初期 HTML もサーバーが描画する ブラウザはその HTML に hydrate(既存の要素を再利用して、イベントや状態を結び付ける)する 島の外には JS を送らない ■ 空の div にマウントする構成(React 17 の例) // サーバーが返すのは空の div だけ。ブラウザ側で中身を一から作る(render) document.querySelectorAll('.search-widget').forEach((el) => { // サーバーからの値は data-params 属性で受け取る ReactDOM.render(<SearchWidget params={JSON.parse(el.dataset.params)} />, el); }); サーバーが返す HTML と、JS 実行後の HTML を並べるとこうなります。 【render(空の div にマウントする構成)】 サーバーが返したHTML JS 実行後 <div class="search-widget"> <div class="search-widget"> (空っぽ) → <form>...</form> ← JS が一から作る </div> </div> 【hydrate(Islands Architecture)】 サーバーが返したHTML JS 実行後 <div> <div> <form>...</form> → <form>...</form> ← 要素は作り直さず、イベントや状態を結び付ける </div> </div> 見分け方:JS を切ったときに島の中身が見えるか React 17 なら ReactDOM.render が render、ReactDOM.hydrate が hydrate10 React 18 以降は、createRoot().render() と hydrateRoot() に置き換わっている11 「島の外には JS を送らない」点だけは Islands と共通 2.2 hydrate で起きていること 調べてみると、hydrate は、サーバーが作った要素をそのまま使い、クリック時の処理や状態を後から結び付ける処理でした。 サーバーから届いた HTML は「見た目」だけで、ボタンを押しても何も起きません。素の JS で、後から動くようにするとこうなります12。 <button id="like">いいね 0</button> // 既にあるボタンを探して、処理を後付けする(要素は作り直さない) const button = document.getElementById('like'); let count = 0; button.addEventListener('click', () => { count += 1; button.textContent = いいね ${count}; }); これが「既存の HTML にイベントを結び付ける」いちばん素朴な形 React の hydrate も考え方は同じで、中では次のことをしている サーバーが返したHTML(見た目だけ。押しても動かない) <form><input><button>検索</button></form> ↓ ReactDOM.hydrate(<SearchForm />, el) ① コンポーネントを実行して、「本来こうなるはず」の設計図をメモリ上に作る ② 設計図と既存の要素を先頭から照らし合わせ、一致した要素を「この部品の担当」として紐付ける ③ onClick などのイベント処理を受け付けられるようにする ④ useState の状態や useEffect を動かし始める ↓ 見た目は同じまま、押せば動く状態になる 設計図と HTML が食い違うと、React 17 は開発モードで警告を出す。テキストの違いは書き換えて合わせるが、属性の違いは直らないことがあるので、食い違いはバグとして直す10:1 ②で作る「設計図 ↔ 実際の要素」の対応表は、正確には Fiber というオブジェクトで、React はこれを使って書き換える要素や、押されたボタンの担当を見つけている13 ■ ブラウザで確かめる Fiber が実際の DOM 要素に付いている様子は、React で作られたサイトなら DevTools で見られます。ここでは React 公式サイトの react.dev を使います。 Chrome で react.dev を開き、DevTools を開く(Mac なら ⌘ + Option + I) DevTools の左上にある矢印のアイコン(要素を選ぶボタン)を押してから、「Learn React」ボタンをクリックする。矢印を押さずにクリックすると、リンク先へページが移動してしまう Console タブで $0(Elements タブで選択中の要素)を実行し、<a … aria-label="Learn React" …>Learn React</a> が表示されることを確かめる 次を実行する Object.keys($0) // => ['reactFiber$9s34k04vvrf', 'reactProps$9s34k04vvrf'] // (2026年10月時点。$ の後ろの文字列はページを開くたびに変わる) $0[Object.keys($0).find(k => k.startsWith('__reactProps$'))] // => {className: 'w-full sm:w-auto …', aria-label: 'Learn React', target: '_self', ref: ƒ, onClick: ƒ, …} reactFiber$… が Fiber への参照、reactProps$… が props。props の中に onClick の関数が入っているのが見える react.dev は React 18 以降で作られているが、キーの名前は React 17 と同じ これらは React 内部の仕組みで、公開された機能ではない。観察だけにとどめ、自分のコードから読み書きしない 2.3 render と hydrate の違いのまとめ 違うのは「要素を一から作るか、サーバーが作ったものを再利用するか」だけでした。Fiber との紐付け(2.2 の②)、イベントの受け付け(2.2 の③)、状態や副作用の開始(2.2 の④)は、どちらも同じです。 ここに「ページの一部か全体か」という範囲の軸を加えると、よく聞く構成は次の4つに整理できます。 render(要素を一から作る) hydrate(サーバーが作った要素を再利用) ページの一部 空の div にマウントする構成 Islands Architecture ページ全体 SPA サーバーで描画して hydrate(Next.js など) どのマスでも、マウントした要素より内側は Fiber と紐付く。違うのは「どこまでを React に任せるか」と「要素を作るか、再利用するか」だけ SPA もページ全体が Fiber と紐付くが、サーバーが返すのは空の div なので、hydrate ではなく render Islands との違いは hydrate する範囲で、Islands は島ごとに別々の React として hydrate し、島の外は React と無関係の HTML のまま。Next.js はページ全体(document)を1つの React として hydrate する 3. Rails Rails のチュートリアル系のサイトや動画で「コントローラーで @ を付けた変数は、ビューでも @ を付けて参照できる」と聞いたものの、裏で何が起きているのか分からず、Rails を学び始めてすぐに疑問に思ったのがきっかけで調べました。 3.1 コントローラーのインスタンス変数がビューに渡る仕組み コントローラーのインスタンス変数がビューで使えるのは、Rails がビュー用のオブジェクトに書き写しているからでした。さらに、テンプレートはそのオブジェクトのメソッドに変換されて実行されていました(Rails 8.1 のソースより)14。 class UsersController < ApplicationController def show @user = User.find(params[:id]) # @ 付きならビューで使える posts = @user.posts # @ なしはこのメソッドの中だけ end end 3.2 そもそも、なぜ書き写す必要があるのか インスタンス変数は、それを持っているオブジェクトの中からしか見えません15。 @user を持っているのは、UsersController のインスタンス テンプレートを実行するのは、それとは別のビュー用オブジェクト 別々のオブジェクトなので、ビューで @user を使うには、ビュー用オブジェクト側にも同じインスタンス変数が必要 そこで Rails が、コントローラーの変数を1つずつビュー用オブジェクトに書き写している 3.3 全体の流れ ブラウザから GET /users/1(ユーザーの詳細画面)のリクエストが届く ↓ ルーティング(resources :users)が users#show に振り分け、params[:id] = "1" にする ① コントローラーを new して、show アクションを実行する ↓ @user = User.find(1) ↓ render(書かなくても自動で呼ばれる) ② 集める view_assigns … コントローラーのインスタンス変数をハッシュにする ↓ { "user" => #<User id: 1> } ③ 作る view_context … ビュー用オブジェクトを new する(ハッシュとコントローラー自身を渡す) (new するクラスの正体は ④ で補足) ↓ ⑤ 書き込む assign … ハッシュの中身を、ビュー用オブジェクトのインスタンス変数にする ↓ ⑥ テンプレート実行 テンプレートを「ビュー用オブジェクトのメソッド」に変換して呼ぶ → テンプレートの @user は、ビュー用オブジェクトの @user 以下、順に見ていきます。 ① コントローラーはリクエストごとに new される ソースを読むと、Rails はリクエストが届くたびにコントローラーを new していました。ルーティングが users#show と判断すると、UsersController.dispatch というクラスメソッドが呼ばれ、その中で new されます(actionpack action_controller/metal.rb)。 def self.dispatch(name, req, res) ... new.dispatch(name, req, res) # UsersController.new して、そのインスタンスで処理する end Web サーバー(Puma など)がリクエストを受け取る ↓ Rails.application.call(env) Rack ミドルウェア(Rails の中で順番に呼ばれる前処理の部品。セッション・Cookie など) ↓ ActionDispatch::Routing::RouteSet#call … ルーティング ↓ Journey::Router#serve … GET /users/1 に合うルートを探す ↓ RouteSet::Dispatcher#serve … "users" → UsersController クラスに変換 UsersController.dispatch(:show, req, res) … Metal に定義されたクラスメソッド ↓ new.dispatch(:show, req, res) … self.new = UsersController.new UsersController のインスタンス#dispatch … 以降はこのインスタンスの上で動く Rack は、Ruby の Web サーバー(Puma など)とアプリ(Rails など)をつなぐ共通ルール。「call(env) を受け取って [ステータス, ヘッダー, 本文] を返す」という約束だけを決めている env は、リクエストの情報(URL、HTTP メソッド、ヘッダーなど)を詰めたハッシュ Puma・Rack ミドルウェア・Rails は別々のサーバーではなく、同じ1つのプロセスの中で動いている リクエストごとに別のインスタンスなので、別のユーザーのリクエストと @user が混ざることはない ② 集める:view_assigns actionpack abstract_controller/rendering.rb のメソッドです。 show の実行後に呼ばれたときの値を、コメントで書き込んでいます。 def view_assigns variables = instance_variables - _protected_ivars instance_variables => [:@_request, :@_response, :@_params, :@_routes, ..., :@user] ← Rails 内部用も混ざっている 配列の引き算で内部用を外す => [:@user] variables.each_with_object({}) do |name, hash| # name = :@user、hash は最初は空の {} hash[name.slice(1, name.length)] = instance_variable_get(name) # "@user" の2文字目以降 "user" をキーに、@user の値を詰める end => { "user" => #<User id: 1> } end 使っているのは、Ruby の標準メソッド2つだけでした16。 メソッド 何をするか instance_variables そのオブジェクトが持つインスタンス変数の名前を、シンボルの配列で返す instance_variable_get(名前) 名前を指定して、その変数の値を取り出す instance_variables はレシーバーを省略しているので、self.instance_variables と同じ。self は①で new されたコントローラーのインスタンス 除外する _protected_ivars は、@_params や @_request など、Rails 内部用の変数を並べた一覧 each_with_object({}) は、空のハッシュを用意して1件ずつ詰め、最後にそのハッシュを返すメソッド つまり、コントローラーの中身を一覧して、@ 付きの変数を「名前 → 値」のハッシュに変換しているだけでした。 ③ 作る:view_context actionview action_view/rendering.rb のメソッドです。 def view_context view_context_class.new(lookup_context, view_assigns, self) end 第1引数:どのテンプレートファイルを使うかを決める条件(探すフォルダ、言語、html / json などの形式) 第2引数:②で作ったハッシュ 第3引数:コントローラー自身(self) ■ self はなぜコントローラーなのか view_context は actionview の ActionView::Rendering モジュールに書かれていますが、それでも self はコントローラーでした。実行時の self は、「そのメソッドがどのファイルやモジュールに書かれているか」ではなく、「ドットの左側にいたオブジェクト(レシーバー)」で決まるからです17。 module Greeting def who = self end class UsersController include Greeting # Greeting のメソッドを、UsersController のインスタンスから呼べるようにする end c = UsersController.new c.who.equal?(c) # => true(レシーバーの c が self になる) UsersController.ancestors.first(2) # => [UsersController, Greeting] include は、メソッドをコピーするのではなく、メソッドを探す経路(ancestors)にモジュールを追加する c.who と呼ぶと、Ruby は UsersController → Greeting の順に who を探し、見つけたメソッドを c の上で実行する Rails の view_context も同じで、コントローラーに include されたモジュールのメソッドです。①で new したコントローラーのインスタンスから、レシーバーを省略して(self.〜 として)呼ばれ続けるので、self はずっとそのインスタンスのままです。そのため第3引数の self には、リクエストを処理しているコントローラー自身が渡されます。 ④ ビュー用のクラスは「無名クラス」 new しているクラス view_context_class は、ActionView::Base を継承した無名クラス(名前のないクラス)でした。 ■ 無名クラスとは Ruby では Class.new でクラスを作れます18。それを定数(大文字で始まる名前)に入れると名前が付き、入れなければ無名クラスになります。 Dog = Class.new { def hi = "hi" } dog = Class.new { def hi = "hi" } Dog.name # => "Dog" dog.name # => nil(中身は同じでも、ローカル変数に入れただけでは名前が付かない) dog.new.hi # => "hi"(変数 dog を通して、普通のクラスと同じように使える) 違うのは名前の有無だけで、継承・include・メソッド定義・new は普通のクラスと同じようにできる 無名クラスでも、使い回すには変数などに入れておく必要がある。ただし変数はクラスを入れておく箱にすぎず、変数名はクラスの名前にならない。箱は何個あってもよく入れ替えもできるので、クラス側には「自分がどの名前で呼ばれているか」が分からない。名前として記録されるのは、最初に入れた定数だけ Rails はビュー用の無名クラスを、コントローラーのクラス側に @view_context_class として入れてキャッシュし、③の view_context_class.new(...) で取り出して new している ■ ヘルパーが呼べる理由 Rails は、この無名クラスにルーティングのヘルパーとアプリのヘルパーを include しています(action_view/rendering.rb)。 Class.new(klass) do include routes.url_helpers(supports_path) # users_path など include helpers # app/helpers/ のメソッド end ビュー用オブジェクトはこのクラスのインスタンスなので、ビューで users_path や自作のヘルパーが呼べる ancestors をたどると、無名クラス → ヘルパーのモジュール → … → ActionView::Base の順にメソッドを探している ⑤ 書き込む:initialize と assign ③の new を呼ぶと、Ruby は次の2つを順に行います。 メモリ上に空のオブジェクトを作る そのオブジェクトの initialize を、new と同じ引数で自動的に呼ぶ action_view/rendering.rb view_context_class.new(lookup_context, view_assigns, self) ↓ ↓ ↓ action_view/base.rb ↓ ↓ ↓ def initialize(lookup_context, assigns, controller) @lookup_context = lookup_context ... assign_controller(controller) # @_controller にコントローラーを入れる ... assign(assigns) # 最後に、②のハッシュを書き込む end def assign(new_assigns) @_assigns = new_assigns new_assigns.each { |key, value| instance_variable_set("@#{key}", value) } end assign は、②の view_assigns のちょうど逆の操作でした。 ② コントローラー側 ⑤ ビュー側 使うメソッド instance_variable_get instance_variable_set 名前の変換 :@user → "user" "user" → "@user" 結果 変数 → ハッシュ ハッシュ → 変数 instance_variable_set("@user", value) は、@user = value を、変数名を文字列で指定して書いたもの19 @user = value と書くと変数名はコードを書いた時点で決まるが、instance_variable_set なら実行中に決められる Rails は開発者がどんな名前の変数を作るか知らないので、ハッシュのキーから名前を組み立てて代入している 2つのオブジェクトの中身の移り変わりを図にすると、こうなります。 【コントローラー】 @_params, @user=#<User name: "山田"> (show を実行した直後) │ │ ② view_assigns:内部用を除いて、名前と値のハッシュにする ▼ { "user" => #<User name: "山田"> } │ │ ③ ビュー用のクラス.new(ハッシュ, self) ▼ 【ビュー用オブジェクト】 (まだ空っぽ) │ │ ⑤ initialize の中で │ assign_controller(self) … @_controller にコントローラーを入れる │ assign(ハッシュ) … "user" を "@user" にして instance_variable_set ▼ 【ビュー用オブジェクト】 @_controller, @user=#<User name: "山田"> │ │ ⑥ テンプレートを実行 ▼ <p>山田さん</p> ⑥ テンプレートは「ビュー用オブジェクトのメソッド」になる ここでいうテンプレートは、app/views/ 配下のファイル全部のことです。 種類 ファイル名の例 アクションのテンプレート users/show.html.erb partial(部品のテンプレート) users/_form.html.erb レイアウト layouts/application.html.erb _ で始まるのは partial だけで、どれも同じ仕組みで動きます。ここでは show.html.erb を例にします。 <p><%= @user.name %>さん</p> Rails はこのファイルを、次の Ruby のメソッドに変換していました。rails runner で ActionView::Template を作り、compiled_source(action_view/template.rb)を表示させた結果です。実際の出力は1行にまとまっているので、見やすく改行してコメントを足しています。 def _app_views_users_show_html_erb__1476774381686002440_15872(local_assigns, output_buffer, &_) @virtual_path = "users/show"; @output_buffer.safe_append='<p>'.freeze; # 固定の HTML はそのまま足す @output_buffer.append=( @user.name ); # <%= @user.name %> は、式の結果を(エスケープして)足す @output_buffer.safe_append='さん</p>'.freeze; @output_buffer # 組み立てた HTML を返す end 実行したコード rails runner で、テンプレートの中身を直接渡して変換させました。ファイルを作ったり DB を書き換えたりはしないので、どの Rails アプリでも試せます。 bundle exec rails runner ' template = ActionView::Template.new( "<p><%= @user.name %>さん</p>", # テンプレートの中身 "app/views/users/show.html.erb", # ファイルのパス(実在しなくてよい) ActionView::Template.registered_template_handler(:erb), locals: [], format: :html, virtual_path: "users/show" ) puts template.send(:compiled_source) # Ruby のメソッドに変換した結果を表示 ' compiled_source は外から呼べない private メソッドなので、send で呼んでいる メソッド名の末尾の数字は、実行するたびに変わる テンプレートは、ファイルのまま実行されるのではなく、1つの def に変換される メソッド名は、ファイルのパスと番号から自動で作られる。先頭の _ は他のメソッドと名前がぶつからないための接頭辞で、partial かどうかとは関係ない Haml でも、生成されるコードの中身が違うだけで仕組みは同じ ■ 変換から実行までの3段階 show.html.erb(ただのテキストファイル) │ 1. 変換:テンプレートエンジン(ERB / Haml)が "def ... end" という Ruby の「文字列」にする ▼ "def _app_views_users_show_html_erb...(local_assigns, output_buffer, &_) ... @user ... end" │ 2. 定義:module_eval で読み込ませる → ビュー用のクラスで使える本物のメソッドになる ▼ ビュー用のクラスのメソッド _app_views_users_show_html_erb... │ 3. 実行:ビュー用オブジェクト.public_send(:_app_views_users_show_html_erb__...) ▼ メソッドの中の self = ビュー用オブジェクト ├ @user → self の @user(⑤で assign した値) └ users_path → self のメソッド(④で include されたヘルパー) module_eval(文字列) は、文字列のコードをクラスの中に書いたものとして実行する。"def ... end" を渡せば、そのクラスにメソッドが追加される public_send(:foo) は、メソッド名をシンボルで指定して呼ぶメソッド。obj.public_send(:foo) は obj.foo と同じ テンプレートのメソッド名は実行中に自動で作られるので、obj._app_views_... のようにコードへ直接書いておけない。そのため、名前を値として受け取れる public_send で呼んでいる klass = Class.new klass.module_eval("def greet = 'こんにちは'") # 文字列のコードから greet メソッドを定義する obj = klass.new name = :greet # 呼びたいメソッドの名前を、値として持っている obj.public_send(name) # => "こんにちは"(obj.greet と同じ) 1と2は最初の1回だけ。2回目以降のリクエストは、定義済みのメソッドを呼ぶだけなので速い テンプレートの @user が読めるのは、Ruby に「メソッドの中の @xxx は、そのメソッドを呼ばれたオブジェクト(self)の変数を指す」というルールがあるから(③の self の話と同じ考え方で、つまり今回はビュー用オブジェクトの @user を指す) 3.4 この仕組みから分かること 書き写されるのは、オブジェクトへの参照でした。中身を丸ごと複製(ディープコピー)しているわけではありませんでした。 Ruby の変数に入っているのは、オブジェクトそのものではなく「そのオブジェクトがどこにあるか」を指す参照です。②⑤の書き写しは、この参照を受け渡しているだけで、オブジェクトを複製する処理(dup など)はどこにもありません。 ref = instance_variable_get(:@user) # ② コントローラーの @user から参照を取り出す hash["user"] = ref # ハッシュに入れる(複製しない) instance_variable_set("@user", ref) # ⑤ ビュー用オブジェクトの @user に入れる(複製しない) そのため、2つの @user は同じオブジェクトを指します。 [コントローラー] @user ──┐ ├──→ #<User name: "山田">(メモリ上に1つだけ) [ビュー用オブジェクト] @user ──┘ 現象 理由 ビューで @user = 別のもの と代入し直しても、コントローラー側は変わらない 変数は別々で、ビュー側の矢印だけが別のものを指し直すから ビューで @user.name = "x" と中身を変えると、コントローラー側から見ても変わる 2つの変数が同じオブジェクトを指しているから partial でも @user が使える partial も同じビュー用オブジェクトのメソッドとして実行されるから ビューでは params も使える ビュー用オブジェクトは params のデータを持たず、⑤で @_controller に入れたコントローラーの params を呼んでいるから(delegate による委譲) @usr と書き間違えてもエラーにならず、nil になる Ruby では、作っていないインスタンス変数を読むと nil が返るから 4. テスト いろいろなテストコードを読むうちに、FactoryBot には create だけでなく build / build_stubbed もあり、データの作り方が3つあると知り、なぜ使い分ける必要があるのか疑問に思ったのがきっかけで調べました。 4.1 FactoryBot の create / build / build_stubbed 調べてみると、DB から読み直す必要がないテストなら、create ではなく build_stubbed を使うと速くなると分かりました。テストデータを作る FactoryBot には、オブジェクトの作り方が3種類ありました20。 user = create(:user) # DB に保存する user = build(:user) # 保存しない(User.new と同じ状態) user = build_stubbed(:user) # 保存しないが、保存済みのふりをする DB への保存 id 向いているテスト 速さ create する 本物 画面表示や検索など、DB から読み直す処理 遅い build しない nil 保存前のバリデーションなど 速い build_stubbed しない 偽の id DB を使わないメソッド 最速 DB に触らない分だけ速いので、create が要らない場面では build / build_stubbed を選ぶ 関連も、既定では親と同じ作り方になる(build なら関連も保存されない) build_stubbed は、DB に保存しないまま、保存済みのように振る舞います21。 user = build_stubbed(:user) user.id # => 1001(偽の id) user.persisted? # => true(「DB に保存済みか」を聞くと、保存済みと答える) user.save # => RuntimeError(本当に DB に触ろうとするとエラー) persisted? の persist は「残り続ける」という意味。「DB に保存されて、プログラムが終わっても残る状態になったか(永続化されたか)」を聞くメソッド build_stubbed はこれに true を返すので、user_path(user) のように保存済みを前提にした URL の生成なども、DB なしでテストできる おわりに 最後まで読んでいただき、ありがとうございました。 脚注 MDN Web Docs「<script>: スクリプト要素」 ↩︎ WHATWG「HTML Living Standard」 ↩︎ W3C「JSON-LD 1.1」 ↩︎ IETF「RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format」 ↩︎ IANA「Media Types」 ↩︎ Ecma International「ECMAScript Language Specification(ECMA-262)」 ↩︎ schema.org「About Schema.org」 ↩︎ IETF「RFC 6839: Additional Media Type Structured Syntax Suffixes」 ↩︎ Jason Miller「Islands Architecture」 ↩︎ React(旧ドキュメント)「ReactDOM」 ↩︎ ↩︎ React「hydrateRoot」「createRoot」 ↩︎ MDN Web Docs「EventTarget: addEventListener() メソッド」 ↩︎ facebook/react v17.0.2「ReactDOMComponentTree.js」「ReactDOMHostConfig.js」 ↩︎ Rails ソースコード(8-1-stable)「actionpack/lib/abstract_controller/rendering.rb」「actionview/lib/action_view/rendering.rb」「actionview/lib/action_view/base.rb」「actionpack/lib/action_controller/metal.rb」「actionview/lib/action_view/helpers/controller_helper.rb」「actionview/lib/action_view/template.rb」 ↩︎ Ruby リファレンスマニュアル「変数と定数(インスタンス変数)」 ↩︎ Ruby リファレンスマニュアル「Object#instance_variables」「Object#instance_variable_get」 ↩︎ Ruby リファレンスマニュアル「変数と定数(擬似変数)」 ↩︎ Ruby リファレンスマニュアル「Class.new」 ↩︎ Ruby リファレンスマニュアル「Object#instance_variable_set」 ↩︎ factory_bot「Build strategies」 ↩︎ thoughtbot/factory_bot v6.5.5「lib/factory_bot/strategy/stub.rb」 ↩︎
@JaeTask @yminsky yes! github.com/react/react/tree/main/compiler/package…
@yminsky here's one example of expect tests, we used these rigorously for react compiler: github.com/react/react/tree/main/compiler/package…
【警告】「npm install」しただけで感染? AI頼みの個人開発者が震えた、2026年10月のサイバー攻撃と脆弱 「えっ、プログラムを書いているだけでパソコンが乗っ取られることがあるの?」 あります。 しかも、自分が悪いプログラムを書いていなくても。 便利だからとインストールした部品に、悪意のあるプログラムが紛れ込んでいる場合があるそうです。 いや、待って。 私はAIにコードを書いてもらって、それを理解するだけでも必死なんですが。 そこにサイバー攻撃まで気を配れと? 初心者への要求スペック、高すぎない??? こんにちは。猫とAIと個人開発を愛する、でやまです。 私はエンジニアとしてはまだまだ勉強中。ChatGPTやCodexに助けてもらいながら、Webサイトを作ったり、アプリを開発したりしています。 最近は夫婦で小さなIT事務所も始めました。 つまり、ようやく「作ること」に慣れてきたくらいの人間です。 そんな私が2026年10月、Node.jsやPHPの重大な脆弱性、世界中で起きているサイバー攻撃の情報を調べてみました。 そして思いました。 これ、エンジニアだけが知っていればいい話じゃないぞ。 AIでアプリを作っている人、ホームページを運営している人、パソコンを仕事に使っている人。 今は誰でも、知らないうちに攻撃の入口を持っている可能性があります。 今回は、2026年10月25日時点で確認できた主な脆弱性と実際の攻撃を、初心者の私なりにわかりやすくまとめます。 専門用語も使います。でも、できる限り説明も付けます。 私と同じく、横文字が3つ並ぶと脳のファンが全力回転する人にも読んでいただければ幸いです。 ※本記事は2026年10月25日時点の公開情報をもとに作成しています。世界中のすべての脆弱性を網羅したものではありません。また、紹介する脆弱性がすべて実際の被害につながったわけではありません。 1. そもそも「脆弱性」ってなんなの? まずはここから。 脆弱性(ぜいじゃくせい)とは、ソフトウェアやシステムに存在するセキュリティ上の弱点のこと。 家に例えるなら、玄関には立派な鍵が付いているのに、裏口の鍵が壊れていたり、窓がきちんと閉まらなかったりする状態です。 攻撃者は、そういう弱点を探します。 しかも困ったことに、脆弱性は私たちが作ったプログラムだけに存在するわけではありません。 WindowsなどのOS Chromeなどのブラウザ Node.jsなどの実行環境 npmからインストールしたライブラリ WordPressなどのWebシステム サーバーやネットワーク機器 このような、日常的に利用しているソフトウェアにも弱点は見つかります。 つまり、私がどれだけ頑張ってコードを書いたところで、使っている部品そのものに脆弱性があれば、影響を受ける可能性があります。 私のコード、まだそこまで高度なことしてないのに、セキュリティだけ世界レベルで戦わされてる。 ちなみに、よく出てくる「CVE-2026-XXXXXX」という文字列。 これは脆弱性に付けられる識別番号です。 CVEは Common Vulnerabilities and Exposures の略。 いわば、脆弱性の管理番号ですね。 また、CVSSという危険度の評価指標もあります。 基本的には数値が高いほど深刻ですが、点数が高いからといって、すべての環境で必ず攻撃が成功するわけではありません。 そして、ここがとても大事。 「脆弱性が見つかった」と「実際に攻撃された」と「情報が盗まれた」は、全部違います。 怖い見出しを見かけたら、まずはこの3つを区別して読みたいところです。 2. 【重要】Node.jsに重大な脆弱性10件。10月21日に緊急修正 個人開発者として、まず目を疑ったのがこれ。 2026年10月21日、Node.jsの公式開発チームが10件の脆弱性に対応するセキュリティ更新を公開しました。 Node.jsとは、JavaScriptというプログラミング言語を、Webブラウザの外でも動かせるようにする仕組みです。 Webアプリを作ったり、開発ツールを動かしたりする際によく使います。 私のようにVS Codeを開いて、AIに指示を出しながらアプリを作っている人にもおなじみです。 そして私はNode.jsのバージョンなんて、普段ほとんど見ていませんでした。 動いてるからヨシ! ……よくないらしい。 今回発見された主な問題 10件の中でも特に気になるのが、CVE-2026-111497です。 これはBrotliというデータ圧縮方式を処理する部分の脆弱性。 圧縮データとは、大きなデータを小さくまとめたものです。旅行に行く前、圧縮袋で洋服を小さくするのと似ています。 ところが、攻撃者が細工したデータをNode.jsに展開させると、内部のメモリが壊れてしまう可能性があります。 結果として、プログラムの異常終了、メモリ内の情報漏えい、最悪の場合は不正なコードの実行につながるおそれがあります。 最後のものは、RCE(Remote Code Execution:リモートコード実行)と呼ばれます。 簡単にいうと、外部からサーバーに勝手な命令を実行させることです。 そのほかにも、DNSというインターネット上の住所案内に関係する問題や、通信相手の電子証明書を確認する仕組みの問題、ファイルへのアクセス制限に関係する問題が修正されています。 公式に公表された10件のCVE番号は以下です。 CVE番号 主な問題 CVE-2026-111497 Brotli圧縮データの処理によるメモリ破損 CVE-2026-105482 DNSの名前解決で別の接続先を扱ってしまう CVE-2026-105483 特殊なIPアドレス表記による証明書検証の問題 CVE-2026-106069 IPアドレスの解釈の違いによる証明書検証の問題 CVE-2026-105481 HTTP通信で不正な応答を受け入れる CVE-2026-106072 非標準的なIP表記による証明書検証の回避 CVE-2026-106074 特定条件で乱数やUUIDが重複・予測可能になる CVE-2026-106070 ZIPファイルの不正な内容を誤認する CVE-2026-106071 ファイルアクセス許可が広く認められる CVE-2026-106073 制限されたファイルの存在や場所を調べられる ただし、これらの問題がすべて同じ深刻度ではありません。 また、Node.jsをインストールしているだけで、必ず外部から乗っ取られるわけでもありません。 使用している機能やアプリの構成によって、実際の影響は変わります。 修正されたNode.jsのバージョン 2026年10月21日に公開された修正版はこちらです。 Node.jsの系列 修正版 22系 22.23.4 24系 24.22.1 26系 26.12.1 Node.jsにはLTSという長期サポート版があります。 新しく開発するなら、サポートされているLTSを選ぶのが基本です。ただし、既存プロジェクトは対応するNode.jsのバージョンを確認してから更新しましょう。 確認方法は簡単。 VS CodeのターミナルやPowerShellで、次を入力します。 node -v バージョンが表示されます。 私のように「昨日まで動いていたものを更新したら、今日から動かなくなった」という悲劇を起こしたくない人は、いきなり大幅にバージョンを変えないでください。 セキュリティ対策のつもりで、自分の手でアプリを壊すのは避けたい。 公式情報:Node.js 2026年10月セキュリティリリース 3. 【実際に攻撃発生】npmの「正規パッケージ」が汚染された 正直、今回の調査で一番怖かったのがこちらです。 2026年8月、400以上のnpmパッケージを巻き込むサプライチェーン攻撃がMicrosoftによって報告されました。 サプライチェーン攻撃とは、ソフトウェアを作るために使う部品や、部品を配布する仕組みを狙う攻撃です。 npmはNode.jsで使うパッケージ管理ツール。 便利な機能を追加したいとき、たとえばこんなコマンドを使います。 npm install パッケージ名 私も使います。 というか、AIが「これをインストールしてください」と言ってくるので、言われるがまま実行してきたこともあります。 AI「こちらのコマンドを実行してください」 私「はい喜んで!」 居酒屋の店員さんより返事がいい。 でも、この攻撃を知ってからは、少し考えるようになりました。 何が起きたの? Microsoftが2026年8月4日に報告した「ChainDrop」という攻撃では、正規のnpmパッケージの公開権限などが悪用され、悪意のあるバージョンが配布されました。 調査対象にはkeyv、flat-cache、cache-managerなどに関連するパッケージも含まれています。 攻撃者のプログラムは、インストール処理の途中で実行される仕組みを利用していました。 感染すると、開発用パソコンや自動ビルド環境に保存された認証情報を探します。 たとえば、 GitHubのトークン npmの公開権限 クラウドサービスの認証情報 開発環境内の秘密情報 などです。 そして、盗んだ権限で別のパッケージに悪意のあるコードを混ぜ、感染を広げることもありました。 つまり、攻撃者が作った怪しい名前のパッケージをインストールするだけが危険なのではなく、普段から利用されている正規パッケージも攻撃対象になるということです。 これは困る。 私のような初心者に、パッケージの中身を全部読んで安全性を証明しろと言われても。 無理です。 猫のごはんの原材料表示ですら、全部は理解できていません。 どんな被害が考えられる? 盗まれたAPIキーで外部サービスを不正利用されたり、GitHubのソースコードを書き換えられたりする可能性があります。 最悪の場合、自分が公開するアプリに悪意のあるコードが混入することも考えられます。 APIキーとは、外部サービスを利用するための秘密の合鍵のようなものです。 これが流出すると、攻撃者が契約しているサービスを勝手に利用したり、許可されている範囲の情報を取得したりできる場合があります。 自分はアプリを作っているつもりなのに、知らない間に攻撃者のための開発環境になっていたら笑えません。 公式に確認された攻撃なので、単なる可能性の話ではありません。 ただし、npmを使った全員が感染したという意味ではありません。汚染された特定のバージョンを、影響を受ける条件で利用した場合の問題です。 Microsoftの調査:ChainDrop supply chain compromise 4. React・Next.jsも危ない? 10月14日に重大な修正 Reactは、WebサイトやWebアプリの画面を作るためによく使われるライブラリです。 Next.jsは、Reactを利用してWebアプリやWebサービスを作るためのフレームワークです。 フレームワークとは、開発に必要な仕組みをあらかじめまとめてくれた土台のこと。 例えるなら、Reactが建物を作るための部品、Next.jsは部品と建築方法がある程度セットになったもの、でしょうか。 私なら最初から完成した家を買いたいところですが、個人開発者なので今日もせっせと組み立てています。 2026年10月14日に修正された問題 React Server Componentsという、サーバー側で動作するReactの仕組みに重大な脆弱性が見つかりました。 識別番号はGHSA-xqc2-xf4c-x3h3。 条件を満たしたアプリへ特殊なデータを送ることで、外部から不正なコードを実行される可能性があります。 さらにNext.jsでも、キャッシュ機能やSVG画像の処理などで、RCEにつながる重大な脆弱性が修正されました。 修正版は次のとおりです。 製品 修正版 Next.js 15系 15.5.28 Next.js 16系 16.4.1 React Server Components 19.0系 19.0.9 React Server Components 19.1系 19.1.10 React Server Components 19.2系 19.2.9 React Server Components 19.3系 19.3.1 ただし、Reactを使っているWebサイトがすべて危険という意味ではありません。 普通のブラウザ上だけで動くReactアプリなど、今回の脆弱性に該当しない構成もあります。 まずは自分のアプリがどの仕組みを使っているのか確認することが大切です。 公式情報:Next.js 2026年10月14日セキュリティ更新 公式情報:React Server Componentsの重大な脆弱性 5. Vite・Axiosにも脆弱性。「開発中だから大丈夫」は通用しないことも 続いて、JavaScript開発者にはおなじみのViteとAxiosです。 Vite:見せてはいけないファイルが見える可能性 Viteは、JavaScriptやReactなどの開発でよく使われるツールです。 2026年10月6日と21日に、複数のセキュリティ問題が公表されました。 中でも気になるのは、開発サーバーが本来公開してはいけないファイルを返してしまう可能性がある問題です。 たとえば、.envというファイル。 これはAPIキーや設定情報などを保存するためによく利用されます。 .envは、ネット上に堂々と置いてよい秘密のメモ帳ではありません。 そもそも秘密のメモ帳を世界中の人が読めるところに置いたら、秘密でも何でもないです。 Viteの問題は、特に開発用サーバーを外部ネットワークからアクセスできる状態にしている場合に注意が必要です。 ただし、通常のローカル開発で外部からアクセスできない設定なら、同じ条件で直接攻撃される危険は抑えられます。 公式情報:Vite Security Advisories Axios:通信に関係する問題 Axiosは、Web APIなどと通信するためによく使われるJavaScriptライブラリです。 2026年9月16日には、通信の制限を回避されたり、特殊なデータで処理が止まったりする複数の問題が公表されています。 たとえば「この通信先にはアクセスしないでね」と設定していたのに、条件によってその制限を回避されるケースです。 主な修正はAxios 1.20.0に含まれています。 もちろんAxiosを使用していなければ、Axios固有の脆弱性は直接関係ありません。 公式情報:Axios Security Advisories 6. PHP2shellって何? WordPressやホームページ管理者は要注意 2026年10月21日、PHPにも重大なセキュリティ修正が公開されました。 PHPは、WordPressなど多くのWebサービスで利用されているプログラミング言語です。 今回公表された脆弱性は、次の2件です。 CVE-2026-109618 CVE-2026-109619 この2件はPHP2shellと呼ばれています。 メモリ不足時のエラー処理や、過去の通信処理で使ったメモリの扱いに問題があり、条件によっては外部から不正なコードを実行される可能性があります。 修正版はこちらです。 PHP系列 修正版 8.2 8.2.35 8.3 8.3.36 8.4 8.4.27 8.5 8.5.12 IPA(情報処理推進機構)も10月22日に注意喚起を公表しています。 ただし、10月21日のIPA発表では、今回の脆弱性を悪用した攻撃は確認していないと説明されています。 つまり、重大な弱点が確認されたことと、実際に攻撃が起きたことは別です。 そこは冷静に読みたいところ。 そしてもう一つ。 WordPressを使っている人は、WordPress本体の更新とPHPの更新を別々に考える必要があります。 WordPressは2026年10月6日に7件のセキュリティ修正を含む7.1.3を公開しています。 PHPだけ更新しても、WordPress本体やプラグインが古いままなら別の問題が残る可能性があります。 ちなみに、私の事務所のホームページはWixで運営しています。 Wixのようなサービスでは、利用者がPHP本体を直接更新するわけではありません。サービス提供者が管理する部分と、私たちが管理する部分を分けて考える必要があります。 何でもかんでも「PHPを更新しなきゃ!」と焦る必要はありません。 まず自分が何を使っているかを確認する。それが大事です。 公式情報:IPA:PHP2shellの脆弱性対策 公式情報:WordPress 7.1.3セキュリティ更新 7. Windows・Chromeも他人事じゃない ここまで読んで、 「私はプログラミングなんてしないし、関係ないかな」 と思った人。 まだ帰らないでください。 WindowsやChromeにもセキュリティ更新が必要です。 Windows 11 Microsoftは2026年10月、複数の脆弱性を修正しました。 特にCVE-2026-96951については、実際の悪用が確認されています。 Windowsの安全機能を回避される可能性がある問題で、IPAも10月14日に緊急の注意喚起を出しています。 対応は基本的にWindows Updateです。 「設定」からWindows Updateを開き、最新の更新を確認しましょう。 更新後に再起動が必要な場合もあります。 私は再起動を後回しにしがちです。 タブを大量に開いて、何を調べていたのか自分でも覚えていないからです。 しかし、セキュリティ更新の再起動は後回しにしない方がよさそうです。 Google Chrome Googleも2026年10月20日にChrome 156を公開しました。 この更新には、353件のセキュリティ修正が含まれています。 353件。 私の未処理タスクより多い気がします。 Chromeは「設定」→「Chromeについて」から更新を確認できます。 Chromeだけでなく、Microsoft EdgeやFirefoxなど、普段使うブラウザも更新しましょう。 公式情報:IPA:2026年10月のMicrosoft製品の脆弱性 公式情報:Google Chrome 2026年10月20日更新 8. 【スマホアプリ開発者は特に注意】アプリを解析して裏側のサーバーを攻撃する「A2B攻撃」 これは個人的に、かなり気になったニュースです。 2026年10月13日、情報通信研究機構(NICT)が、公開Androidアプリを解析してサーバーを攻撃する手法について注意喚起を公表しました。 名前はA2B攻撃(App-to-Backend Attack)。 Appはアプリ、Backendはアプリの裏側で動くサーバーのこと。 たとえば、スマホアプリで会員登録するとき、入力した情報を保存するサーバーがありますよね。 攻撃者は公開されているアプリを調べて、裏側でどんな通信をしているのか、どんなサーバーにつながっているのかを分析します。 そして、その情報をもとにサーバーの弱点を狙います。 表側のアプリを調べて、裏側の秘密を探ろうとするわけです。 私はまだアプリをストアで公開する準備をしている段階ですが、これは覚えておくべきだと思いました。 アプリの中に秘密のAPIキーを直接入れてしまったり、サーバー側のアクセス制限を適切に設定していなかったりすると、危険が大きくなる場合があります。 大事なのは、アプリ内に書いた情報は解析される可能性があるという前提で設計すること。 NICTは、同様の問題がiOSアプリやWebサービスでも起こり得ると説明しています。 AIのおかげでアプリを作るハードルは下がりました。 でも、アプリを公開するなら、利用者の情報を守る責任も付いてきます。 AIに「アプリ作って!」と頼むだけでは、セキュリティ対策まで自動的に完璧になるわけではない。 耳が痛いですね。 公式情報:NICT:アプリ解析からのサーバ攻撃への注意喚起 9. 実際に企業が攻撃されたら、何が起きるの? ここからは仮定の話ではありません。 2026年に企業が公表した、実際の被害について紹介します。 NEC:委託先の攻撃から顧客システムにも被害 2026年10月20日、NECは委託先へのサイバー攻撃によって、顧客システムへの不正アクセスや情報漏えいが発生したと公表しました。 委託先の正規アカウントの認証情報が盗まれ、それを使って顧客システムへアクセスされたとのこと。 結果として、ファイルの暗号化、システム停止、個人情報やシステム関連情報の漏えいが確認されています。 ここで怖いのは、自分の会社が直接攻撃されなくても、仕事を任せている相手から被害が広がる可能性があることです。 私は小さなIT事務所を運営しています。 将来、お客様のホームページやアカウントをお預かりする機会もあります。 つまり、私の管理が甘かった場合、私だけでなくお客様にも迷惑をかける可能性がある。 これは本当に気を付けたいところです。 公式情報:NEC:委託先への不正アクセスに起因する被害について タイムズカー:本人確認書類の画像データにも影響 2026年10月23日、タイムズモビリティはWebシステムへの不正アクセスに関する調査結果を公表しました。 その中では、本人確認書類の画像データ約146万件について、外部送信の痕跡が確認されたと説明されています。 ただし、画像データは暗号化された状態で保管されていたため、外部送信の痕跡があることと、画像の中身をそのまま閲覧されたことは同じではありません。 この区別は大切です。 また、同社は被害を受けたあと、本人確認書類の画像の保有方法についても見直しています。 この事件から私が学んだのは、必要以上の個人情報を長期間保存すること自体が、事業者にとってリスクになるということです。 これは、小さなWebサービスでも無関係ではありません。 公式情報:タイムズモビリティ:不正アクセス調査結果 PingCAP:GitHubの認証情報が侵入経路の一部に 2026年10月23日、データベース技術を提供するPingCAPもセキュリティ事故について公表しました。 侵害されたGitHubアカウントや露出していた認証情報を利用され、社内システムへの不正アクセスや情報のコピーが行われたと説明しています。 開発者としては、かなり気になる事件です。 GitHubはソースコードを保存するだけでなく、自動ビルドや公開作業にも使えます。 つまり、GitHubの認証情報が漏れると、被害がソースコードの閲覧だけで済まない場合があります。 GitHubのパスワードを適当にしていたら、未来の自分が泣く。 そういうことですね。 公式情報:PingCAP Trust Hub 10. ハッカーはどうやって攻撃してくるの? ここまで読むと、攻撃者というのは映画に出てくるような、黒いパーカーを着た天才プログラマーばかりだと思うかもしれません。 でも、現実の攻撃はもっと地味で、もっと厄介です。 たとえば、次のような方法があります。 フィッシング 本物の会社やサービスを装ったメール・SMS・Webサイトなどで、パスワードや認証情報を盗み取る手口です。 認証情報の悪用 盗まれたIDやパスワードを使って、正規の利用者としてログインします。 脆弱性の悪用 更新されていないソフトウェアの弱点を使い、サーバーやネットワークへ侵入します。 サプライチェーン攻撃 正規のソフトウェアやライブラリの配布経路に悪意のあるコードを紛れ込ませます。 そして、被害を受けた企業に金銭を要求する攻撃もあります。 ランサムウェアと二重脅迫 ランサムウェアとは、データを暗号化して使用できなくするなどして、身代金を要求するマルウェアの一種です。 さらに近年は、データを盗んだうえで、 「お金を払わなければ公開する」 と脅す手口もあります。 これを二重脅迫(ダブルエクストーション)と呼びます。 2026年10月、米国CISAとFBIなどは、ランサムウェア集団SafePayについて共同注意喚起を公表しました。 2024年8月以降、少なくとも600組織に影響があったと報告されています。 この集団は、認証情報の窃取、VPNの悪用、未修正の脆弱性を使った侵入などを行い、データの窃取や暗号化を通じて金銭を要求する手口が確認されています。 怖いですね。 でも、これを知ったからといって、パソコンを窓から投げ捨てるわけにもいきません。 まだローンの残っているパソコンだったら、物理的な被害が増えるだけです。 私たちにできるのは、攻撃の入口を減らして、万が一の被害を小さくすることです。 参考:CISA・FBIなどによるSafePayへの共同注意喚起 11. AI頼みの個人開発者が、今日からできること ここまで読んで、 「わかった。危ないのはわかった。で、何をすればいいの?」 となった人。 大丈夫です。 私も同じです。 専門用語を50個覚えるより、まずは実際に使っているパソコンや開発環境を確認しましょう。 ① Windowsとブラウザを更新する まずは一番身近なところから。 Windows Updateを確認し、ChromeやEdge、Firefoxなども更新します。 必要な再起動も忘れずに。 ② Node.jsとnpmのバージョンを確認する VS CodeのターミナルやPowerShellで、次を実行します。 node -v npm -v 使用しているバージョンが表示されます。 Node.jsは公式のセキュリティ情報と照らし合わせて、修正が必要か確認しましょう。 ただし、プロジェクトが必要とするバージョンを調べずに、いきなり最新版へ変更するのは避けたいところです。 ③ npmの脆弱性を確認する 開発中のプロジェクトフォルダで、次のコマンドを実行します。 npm audit 既知の脆弱性を持つ依存パッケージがあるか確認できます。 さらに、 npm outdated で更新候補を調べられます。 ただし、npm auditで問題が表示されなくても、未知のマルウェアまで完全に見つけられるわけではありません。 また、何も考えずにnpm audit fix --forceを実行すると、互換性のないバージョンへ更新される場合があります。 セキュリティ対策でアプリが起動しなくなったら、それはそれで泣きます。 まずは結果を確認し、必要な修正だけをテストしながら行いましょう。 ④ GitHubのセキュリティ設定を確認する GitHubには、依存パッケージの脆弱性を知らせるDependabot alertsや、秘密情報の漏えいを検出するSecret scanningなどがあります。 利用できる機能は、リポジトリやプランなどの条件によって異なります。 GitHubのアカウントには多要素認証を設定し、不要なアクセストークンや、必要以上の権限を持つトークンがないか確認しましょう。 ⑤ APIキーをソースコードに直接書かない AIを使っていると、コードが動くことを優先してしまいがちです。 しかし、公開するソースコードやブラウザから読み取れるプログラムに、秘密のAPIキーを直接埋め込むのは危険です。 .envや各サービスの秘密情報管理機能などを利用して、用途に合った場所で管理することが大切です。 なお、.envなら何でも安全というわけではありません。公開環境への配置方法やアクセス制限も重要です。 すでに秘密情報を公開してしまった場合は、ファイルから削除するだけでなく、その認証情報を無効化・再発行することも必要です。 一度流出した合鍵は、隠し直しても合鍵のままです。 ⑥ AIが提案したコマンドをそのまま実行しない これが私には一番刺さります。 AIが出してくれたコードやコマンドはとても便利です。 でも、AIが提案した内容が常に安全とは限りません。 特に、新しいパッケージのインストール、ファイル削除、権限変更、外部サイトからのスクリプト取得などは、実行前に内容を確認するようにしましょう。 わからなければ、別のAIに「このコマンドは何をする? 危険なところはない?」と確認する方法もあります。 ただし、AI同士が同じ間違いをすることもあるので、重要な変更は公式ドキュメントと照らし合わせる必要があります。 AI「安全です」 別のAI「安全です」 私「じゃあ安全……なのか?」 多数決で決める話ではありません。 ⑦ バックアップを取る 大事なソースコード、設定、データは定期的にバックアップしましょう。 GitHubへpushしているだけでは、すべてのバックアップが十分とは限りません。 データベースや、GitHubに保存していない設定、公開サービスのデータなども確認が必要です。 特に、バックアップ先まで同じアカウントで削除できる構成だと、アカウント乗っ取りによって一緒に消される危険があります。 別の場所にも保管し、必要なときに復元できるか確認することが大切です。 12. 「でも私、Wixでホームページ作ってるだけだよ?」という人へ Wixのようなホームページ作成サービスを使っている人は、Node.jsやPHPの更新を自分で実施する必要があるとは限りません。 基盤部分はサービス提供者が管理しているためです。 ただし、それで完全に安全という意味ではありません。 たとえば、 Wixアカウントの乗っ取り 不要な共同管理者の放置 外部サービスとの連携設定 問い合わせフォームで扱う個人情報の管理 偽メールによる認証情報の窃取 などは、サイト運営者自身にも関係する問題です。 私もWixでホームページを運営しているので、今回の調査を機に管理者や連携アプリの設定を確認しようと思いました。 サーバーの難しい設定を自分で管理しなくてよいのはありがたいことです。 しかし、管理しているアカウントそのものを盗まれたら大変です。 「Wixだから安全」「WordPressだから危険」といった単純な話ではありません。 どのサービスにも、サービス提供者が守る部分と、利用者が守る部分があります。 そこを理解しておきたいですね。 13. そもそも、私たち初心者はどうやって勉強すればいいの? ここまで読んで、少し疲れませんでしたか? 私は疲れました。 脆弱性、CVE、RCE、npm、TLS、DNS、CI/CD、APIキー、サプライチェーン……。 もうアルファベットはお腹いっぱいです。 でも、今回調べていて気付いたことがあります。 全部の専門用語を暗記する必要はない。自分が使っているものを把握することが、最初の一歩になる。 自分のパソコンはWindowsなのかLinuxなのか。 自分のアプリはNode.jsを使っているのか。 Next.jsなのか、普通のJavaScriptなのか。 npmから何をインストールしているのか。 GitHubの認証情報はどこで管理しているのか。 そういう基本情報を一つずつ知っていくことが大切なんだと思います。 そして、これはAIを使って開発している人ほど意識したいことです。 AIは、コードを書いたり、エラーを調べたり、わからない言葉を説明したりしてくれます。 でも、公開するアプリに責任を持つのは、最終的には開発者です。 私は「AIが作ったからわかりません」で済ませないようにしたい。 もちろん、まだまだわからないことだらけです。 それでも、「これは何のために必要なの?」「どうしてこのライブラリを使うの?」「このコードに危険はないの?」と確認する習慣は付けられます。 完成させることと、安全に使ってもらうこと。 個人開発者として、両方を目標にしたいと思いました。 14. 最後に。怖がるより、確認しよう。 今回の調査をして、正直少し怖くなりました。 自分が一生懸命作ったアプリ。 時間をかけて整えたホームページ。 AIに何度も「そこじゃない」と言いながら修正してもらったコード。 それらが攻撃で壊されたり、利用者に迷惑をかける原因になったりするのは避けたいものです。 でも、怖いからといって開発をやめる必要はありません。 セキュリティの脆弱性は、発見され、修正され、対策が進められています。 大切なのは、「何が危険なのか」「自分が使っているものに関係するのか」「何を更新すればよいのか」を知ること。 私のような初心者でも、まずはバージョンを確認するところから始められます。 そして、全部を一日で完璧にする必要もありません。 体調のいい日に一つずつ。 今日できなければ、別の日に。 ただし、実際に悪用されている重大な脆弱性が自分の公開システムに該当する場合は、放置せず、必要に応じて公開停止や管理者への相談なども含めて早急に対応したいところです。 AIにコードを書いてもらう時代だからこそ、そのコードを使う私たちにも最低限のセキュリティ知識が必要なのだと思います。 ちなみに、この記事を書いている私は、セキュリティの専門家ではありません。 AIと一緒に個人開発をしている、まだまだ勉強中の人間です。 だからこそ、同じ初心者の方に、難しい言葉をできるだけかみ砕いて伝えたいと思い、今回の記事を書きました。 最後にひとつだけ。 「昨日まで動いていたから、今日も安全」とは限りません。 私自身、これを忘れないようにしたいと思います。 さて、まずは自分のNode.jsのバージョンでも確認してきます。 もし古かったら? ……そのときはAIに相談しながら、慎重に更新します。 AI頼みは続くんかい。 続きます。 でも、丸投げは少しずつ卒業したい。 そんな個人開発者のセキュリティ勉強記録でした。 最後まで読んでいただき、ありがとうございました。 参考資料・一次情報 この記事は2026年10月25日時点で公開されていた各開発元、政府・公的機関、被害企業の情報をもとに作成しました。 開発環境・ソフトウェア Node.js公式:2026年10月のセキュリティリリース React公式:React Server Componentsの重大な脆弱性 Next.js公式:2026年10月のセキュリティ更新 Vite公式:セキュリティ情報一覧 Axios公式:セキュリティ情報一覧 WordPress公式:7.1.3セキュリティリリース Google Chrome公式:2026年10月20日更新 国内の公的機関 IPA:PHP2shellの脆弱性対策 IPA:2026年10月のMicrosoft製品の脆弱性対策 IPA:重要なセキュリティ情報一覧 NICT:アプリ解析からのサーバ攻撃への注意喚起 実際の攻撃・企業被害 Microsoft Security:ChainDropサプライチェーン攻撃 NEC:委託先への不正アクセスに起因する被害 タイムズモビリティ:不正アクセス調査報告 PingCAP:Trust Hub CISA・FBIなど:SafePayランサムウェアへの注意喚起 ※実際の被害については、各社が公表した範囲を記載しています。未確認の攻撃原因や被害規模を断定するものではありません。 #サイバーセキュリティ #個人開発 #生成AI #プログラミング初心者 #Nodejs