悪意ある攻撃対策
ある日突然データが改ざんされた??
運用していたテニスサークルのデータが改ざんされていました。
色々調べるとセキュリティが甘かった。
セキュリティは色々、触ったけど、firebaseは従量課金のため膨大な読み込みがなされた場合、費用が膨大になる可能性があります。
最悪の事態を回避するため、一日のアクセス数の上限を設定することとしました。
以下は忘備録です。
functionsでの設定
アクセス数をdatabase のsystemコレクションのglobalAccessといドキュメントに保存して管理を行います。
コレクションとドキュメントは自動で生成されないので、手動で設定します。
ドキュメントには、lastResetTimeというTimestampのものと、requestCountというNumberのプロパティが必要です。
この仕組みでは、ドキュメントにアクセスされる度に、記録されるため結果的にアクセス数は二倍となります。
index.js
const cors = require("cors")({ origin: true });
const db = admin.firestore();
exports.limitFirestoreAccess = functions.https.onRequest(async (req, res) => {
cors(req, res, async () => {
try {
const now = new Date();
const globalRef = db.collection("system").doc("globalAccess");
const doc = await globalRef.get();
let requestCount = 0;
let lastResetTime = now;
if (doc.exists) {
const data = doc.data();
requestCount = data.requestCount || 0;
lastResetTime = data.lastResetTime;
if (lastResetTime) {
const lastResetTimeDate = lastResetTime.toDate();
const elapsedTime = now.getTime() - lastResetTimeDate.getTime();
const oneHour = 60 * 60 * 1000;
if (elapsedTime >= oneHour) {
console.log("リセット条件を満たしたため、requestCount をリセット");
requestCount = 0;
lastResetTime = now;
}
}
}
if (requestCount >= 3000) {
return res
.status(429)
.json({ error: "⚠️ Firestore のアクセスが制限されています!" });
}
requestCount += 1;
console.log(`Updating access count: ${requestCount}`);
await globalRef.set(
{
requestCount: requestCount,
lastResetTime: now,
},
{ merge: true }
);
res.status(200).json({ message: "✅ アクセス許可" });
} catch (error) {
console.error("Error updating access count:", error);
res.status(500).json({ error: "内部サーバーエラー" });
}
});
});
上記はfunctionsにデプロイします。
##チェックする関数を作成
CheckAccessLimit.js
export const CheckAccessLimit = async () => {
try {
const response = await fetch(
"https://us-central1-circle-base-a3abd.cloudfunctions.net/limitFirestoreAccess",
{
method: "GET",
mode: "cors",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error("Network response was not ok");
}
const contentType = response.headers.get("content-type");
let data;
if (contentType && contentType.includes("application/json")) {
data = await response.json();
} else {
data = await response.text();
}
if (typeof data === "string") {
console.log("Response is a string:", data);
} else {
console.log("Received JSON data:", data);
}
if (data.error) {
console.error("members", data.error);
return false;
}
return true;
} catch (error) {
console.error("アクセス制限の確認中にエラーが発生しました:", error);
return false;
}
};
##呼び出し
この例では、fetchEventData.jsでeventコレクションのデータを呼び出しています。
fetchEventData.js
// src/utils/fetchEventData.js
import { parseISO } from "date-fns";
import { collection, onSnapshot, query, where } from "firebase/firestore";
import { db } from "../firebase"; // Firebaseの設定がされているファイルからdbをインポート
import { convertDurationToMinutes } from "./convertDurationToMinutes";
import { CheckAccessLimit } from "../utils/CheckAccessLimit";
CheckAccessLimit();
export const subscribeToEventData = async (callback) => {
try {
const eventCollection = collection(db, "events");
const canAccess = await CheckAccessLimit();
if (!canAccess) {
console.log("アクセス制限がかかっているため、データ取得を中止します。");
alert(
"アクセス数が上限のためアクセスができません。しばらくしてからアクセスしてください。"
);
return;
}
const unsubscribe = onSnapshot(eventCollection, (snapshot) => {
const eventList = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(), // すべてのデータを取得
}));
if (eventList.length > 0 && eventList[0].jsonData) {
const parseEventList = JSON.parse(eventList[0].jsonData);
const eventId = eventList[0].id;
callback(parseEventList, eventId); // idもコールバックに渡す
} else {
console.warn("No valid jsonData found in members collection.");
}
});
return unsubscribe; // クリーンアップ用
} catch (error) {
console.error("Error subscribing to event data:", error);
return () => {}; // 何も行わないクリーンアップ関数を返す
}
};