مرز اعتماد در API؛ اعتبارسنجی، محدودیت ورودی و آمادگی انتشار
در این درس ۶ بخش
برای یک برنامهٔ محلی آموزشی هم قرارداد ورودی لازم است. درخواست میتواند از مرورگر یا PowerShell بیاید؛ سرور نباید به فرم اعتماد کند. پیام باید متنی، غیرخالی و محدود باشد و فیلدی مثل admin نباید وارد مدل شود. این درس تابعی خالص برای بررسی بدنهٔ کوچکِ از قبل دریافتشده میسازد. این تابع بهتنهایی سامانهٔ امن عمومی یا محافظ دریافت شبکه نیست؛ حدود مسئولیتش را دقیق مینویسیم.
اول قرارداد، بعد تبدیل
قرارداد مثال: ورودی تابع متن JSON با حجم حداکثر ۱۰۲۴ بایت UTF-8 است؛ ریشه باید شیء سادهٔ JSON باشد، نه null یا آرایه. فقط فیلد message پذیرفته میشود؛ مقدار آن پس از trim باید بین یک تا ۸۰ نقطهکد باشد. این شمارش معادل تعداد نشانههای دیداری برای همهٔ ترکیبهای Unicode نیست. سقفها انتخاب آموزشیاند، نه قانون عمومی پیامرسانها. تابع خروجی تازه با همان یک فیلد میسازد و هیچ دادهای را ذخیره نمیکند.
کد را در فایل message-contract.mjs ذخیره کنید. تابع صادر میشود تا بتوانیم مستقل از HTTP آن را آزمایش کنیم. حذفکردن بیخبر فیلد اضافه را انتخاب نکردهایم؛ در این قرارداد درخواست دارای فیلد ناشناخته رد میشود تا اشتباه کلاینت آشکار بماند.
export function parseMessage(raw) {
if (typeof raw !== 'string') throw new Error('body_must_be_text');
if (Buffer.byteLength(raw, 'utf8') > 1024) throw new Error('body_too_large');
let value;
try { value = JSON.parse(raw); }
catch { throw new Error('invalid_json'); }
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
throw new Error('body_must_be_object');
}
const keys = Object.keys(value);
if (keys.length !== 1 || keys[0] !== 'message') {
throw new Error('unexpected_fields');
}
if (typeof value.message !== 'string') throw new Error('message_must_be_text');
const message = value.message.trim();
if ([...message].length < 1 || [...message].length > 80) {
throw new Error('message_length');
}
return { message };
}آزمون شکست بهاندازهٔ نمونهٔ موفق مهم است
import assert from 'node:assert/strict';
import { parseMessage } from './message-contract.mjs';
assert.deepEqual(parseMessage('{"message":" سلام "}'), { message: 'سلام' });
assert.throws(() => parseMessage('{bad'), /invalid_json/);
assert.throws(() => parseMessage('null'), /body_must_be_object/);
assert.throws(() => parseMessage('[]'), /body_must_be_object/);
assert.throws(() => parseMessage('{"message":" "}'), /message_length/);
assert.throws(() => parseMessage('{"message":"hi","admin":true}'), /unexpected_fields/);
assert.throws(() => parseMessage(JSON.stringify({ message: 'a'.repeat(81) })), /message_length/);
assert.throws(() => parseMessage('x'.repeat(1025)), /body_too_large/);
console.log('message contract: 8 checks passed');این بخش را در message-contract-check.mjs کنار فایل تابع ذخیره و با node .\message-contract-check.mjs اجرا کنید. خروجی باید message contract: 8 checks passed باشد. یک آزمون برای مقدار عددی message اضافه کنید؛ باید message_must_be_text بگیرد. آزمونها بخش مشخصی از قرارداد را پوشش میدهند و اثبات امنیت همهٔ برنامه نیستند. اینکه نمونهٔ حملهای را در فهرست آزمون نداشتیم، دلیل ایمنبودن در برابر آن نیست.
دریافت شبکه، پیش از این تابع شروع میشود
سرور واقعی نباید بدنهای بینهایت را کامل در حافظه جمع کند و بعد فقط این تابع را صدا بزند. هنگام دریافت جریان باید سقف بایت اعمال شود و درخواست بیش از حد، طبق قرارداد رد شود. Content-Length اعلامشده نیز جای شمارش واقعی بایتهای دریافتی را نمیگیرد. نوع محتوا، روش مجاز، سقف تعداد رکوردها و زمان دریافت هم تصمیمهای جدا هستند. در پروژهٔ نهایی دوره، محدودیت بدنه را در لایهٔ HTTP میبینید؛ این درس دلیل وجود آن لایه را روشن میکند.
requestTimeout زمان دریافت کل درخواست و headersTimeout زمان دریافت هدرها را کنترل میکند؛ اینها مهلت اجرای هر تابع یا پرسوجوی پایگاه داده نیستند. برای کارهای پاییندست مهلت و لغو مستقل لازم است. مقدار مناسب به حجم مجاز، نوع سرویس و واسط جلوی آن بستگی دارد؛ عدد یک آموزش را برای همهٔ سامانهها کپی نکنید.
CORS، ورود و مجوز سه مسئولیت متفاوتاند
CORS دربارهٔ دسترسی کد مرورگر به پاسخهای مبدأ دیگر است؛ جای تشخیص هویت یا کنترل مالکیت را نمیگیرد. ابزار خط فرمان الزامهای CORS مرورگر را مثل همان مرورگر اجرا نمیکند. بنابراین دیدن موفقیت درخواست در PowerShell و شکست آن در صفحهٔ وب میتواند نشانهٔ مسئلهٔ CORS باشد، ولی بازکردن همهٔ مبدأها راه عمومی حل امنیت نیست. ابتدا مبدأ رابط و API و قرارداد دسترسی را مشخص کنید. در تمرین محلی فعلی، نیاز به حساب واقعی یا توکن محرمانه نداریم.
احراز هویت میپرسد این درخواست متعلق به چه کسی است؛ مجوز میپرسد آیا همان شخص اجازهٔ این عملیات روی این منبع را دارد. وجود شناسهٔ یک کار در نشانی، مالکیت آن را ثابت نمیکند. همچنین دادهٔ معتبر لزوماً برای نمایش HTML امن نیست؛ خروجی باید متناسب با جای مصرف ساخته شود. در رابط ساده، برای متن کاربر از textContent استفاده کنید و آن را در innerHTML نگذارید. اگر ذخیرهٔ پایگاه داده اضافه شد، پرسوجوی پارامتری لازم است؛ چسباندن ورودی به متن پرسوجو را از روی مثالهای ساده عادی نکنید.
از محیط آموزشی تا سرویس عمومی
پروژهٔ این دوره روی 127.0.0.1، با دادهٔ ساختگی و بدون ورود واقعی اجرا میشود. پیش از انتشار عمومی باید نیازهای همان محصول بررسی شود: TLS، ورود و مجوز، نگهداری امن تنظیمات، محدودیت نرخ و حجم، پایگاه داده و پشتیبان، ثبت رخداد با حذف اطلاعات حساس، وابستگیهای بهروز و رفتار خاموششدن. اینها صرفاً گزینهٔ تزئینی نصب نیستند. بهجای خرید فوری VPS، نخست برنامهٔ محلی و آزمونهایش را کامل کنید و سپس معماری میزبانی را انتخاب کنید.
تمرین، پاسخ و معیار تحویل
قرارداد را تغییر دهید تا فیلد اختیاری category با فقط دو مقدار study و work بپذیرد. پاسخ راهنما: فهرست کلیدهای مجاز را صریح کنید، وجود message را همچنان الزامی بدانید، مقدار category را مستقل بررسی و فقط فیلدهای پذیرفته را بسازید. حالت بدون دسته، دستهٔ درست، دستهٔ ناشناخته و فیلد اضافه را آزمون کنید. معیار تحویل، نتیجهٔ قابل پیشبینی برای ورودی مجاز و شکست روشن برای ورودی نامجاز است؛ افزودن رمز یا حساب آزمایشی واقعی در این درس لازم نیست.
Node.js، شیوههای امنیتی: https://nodejs.org/en/learn/getting-started/security-best-practices
Node.js، محدودیتهای دریافت HTTP: https://nodejs.org/api/http.html
MDN، سازوکار CORS: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
تألیف و کد اختصاصی: تحریریه درسآوند.
متن درسهای عمومی را بدون ورود بخوانید.