Безопасность и лучшие практики
Этот раздел содержит рекомендации по безопасной интеграции ShopStory Public API в ваше приложение.
🔐 Защита applicationId
Не публикуйте учетные данные
Ваш applicationId — это идентификатор для доступа к API. Защищайте его:
❌ Небезопасно - applicationId в коде
const APP_ID = 'my-secret-app-id-12345'; // Плохо!
✅ Безопасно - использование переменных окружения
# .env файл (добавьте в .gitignore!)
SHOPSTORY_APP_ID=your-app-id-here
# Затем в коде
const APP_ID = process.env.SHOPSTORY_APP_ID;
Контрольный список
- ✅ Храните
applicationIdв переменных окружения - ✅ Добавьте
.envв.gitignore - ✅ Используйте разные applicationId для dev/staging/production
- ✅ Не коммитьте конфигурационные файлы с ID в публичные репозитории
- ✅ Ротируйте applicationId при подозрении на компрометацию
- ✅ Старайтесь использовать нечитаемые значения (
shop_6u9f0k31) вместо брендовых имён - ✅ Добавьте собственный WAF, IP-фильтры или проверку реферера на прокси
🛡️ Валидация входных данных
Проверка параметров
Всегда валидируйте параметры перед отправкой в API:
Валидация на сервере
function validatePaginationParams(limit, offset) {
// limit: 1-100
const validLimit = Math.max(1, Math.min(100, parseInt(limit, 10) || 20));
// offset: >= 0
const validOffset = Math.max(0, parseInt(offset, 10) || 0);
return { limit: validLimit, offset: validOffset };
}
// Использование на вашем backend-прокси
const { limit, offset } = validatePaginationParams(userLimit, userOffset);
const params = new URLSearchParams({
limit: limit.toString(),
offset: offset.toString()
});
const headers = {};
if (process.env.SHOPSTORY_API_TOKEN) {
headers.Authorization = `Bearer ${process.env.SHOPSTORY_API_TOKEN}`;
} else {
params.set('applicationId', APP_ID);
}
Безопасная работа с feedProductId
Валидация ID товара
function validateFeedProductId(id) {
// Разрешаем только буквы, цифры, дефисы и подчеркивания
const pattern = /^[a-zA-Z0-9_-]+$/;
if (!id || !pattern.test(id)) {
throw new Error('Invalid feedProductId format');
}
return id;
}
// Безопасное использование
try {
const productId = validateFeedProductId(userInput);
const url = new URL('https://app.shopstory.live/v3/streams');
url.searchParams.set('applicationId', APP_ID);
url.searchParams.set('feedProductId', productId);
} catch (error) {
console.error('Invalid input:', error.message);
}
Backend-прокси
Всегда выполняйте запросы к ShopStory на сервере, чтобы скрыть applicationId и применить дополнительные проверки.
proxy.js
import fetch from 'node-fetch';
export async function fetchStreams(req, res) {
const { limit = '20', offset = '0' } = req.query;
const url = new URL('https://app.shopstory.live/v3/streams');
url.searchParams.set('applicationId', process.env.SHOPSTORY_APP_ID);
url.searchParams.set('limit', limit);
url.searchParams.set('offset', offset);
const upstream = await fetch(url.toString());
const data = await upstream.json();
// Можно внедрить проверку WAF/IP-reputation здесь:
// if (!isAllowed(req)) {
// return res.status(403).json({ status: 403, body: { error: { code: 'Forbidden' } } });
// }
res.status(200).json(data);
}
🌐 Безопасность на фронтенде
Content Security Policy (CSP)
При встраивании ShopStory плеера настройте CSP:
HTML метатег CSP
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
frame-src https://watch.shopstory.live;
connect-src https://app.shopstory.live;
img-src https://cdn.shopstory.live https:;
style-src 'self' 'unsafe-inline';">
Nginx заголовок CSP
add_header Content-Security-Policy "frame-src https://watch.shopstory.live; connect-src https://app.shopstory.live;" always;
Security headers checklist
Strict-Transport-Security: max-age=63072000; includeSubDomains; preloadX-Frame-Options: DENYилиContent-Security-Policy: frame-ancestors 'none'X-Content-Type-Options: nosniffReferrer-Policy: no-referrer-when-downgradePermissions-Policy: camera=(), geolocation=(), microphone=()X-XSS-Protection: 0
Настройте заголовки на уровне CDN или edge-прокси, чтобы закрыть распространённые браузерные векто ры.
Защита от XSS при рендеринге данных
❌ Уязвимый код
// Опасно! Пользовательские данные могут содержать <script>
element.innerHTML = stream.name;
✅ Безопасный код
// Используйте textContent или санитизацию
element.textContent = stream.name;
// Или с React
<div>{stream.name}</div> // React автоматически экранирует
🔗 Безопасность Deep Links
Валидация translationId
Проверяйте формат ID перед созданием deep link:
Безопасная генерация deep link
function createSafeDeepLink(translationId, startTime = 0) {
// Валидация: только буквы, цифры, дефисы
if (!/^[a-zA-Z0-9-]+$/.test(translationId)) {
throw new Error('Invalid translation ID');
}
// Валидация времени
const safeTime = Math.max(0, parseInt(startTime) || 0);
// URL encoding обязателен!
const encodedId = encodeURIComponent(translationId);
return `https://watch.shopstory.live/stream/${encodedId}?t=${safeTime}`;
}
// Использование
try {
const link = createSafeDeepLink(userProvidedId, 120);
window.location.href = link;
} catch (error) {
console.error('Cannot create deep link:', error);
}
Защита от Open Redirect
Безопасная обработка UTM параметров
function addSafeUTMParams(baseUrl, utmSource) {
const url = new URL(baseUrl);
// Whitelist разрешенных источников
const allowedSources = ['app', 'website', 'email', 'push'];
if (allowedSources.includes(utmSource)) {
url.searchParams.set('utm_source', utmSource);
}
// Никогда не берите redirect URL от пользователя
return url.toString();
}
📊 Безопасная обработка ответов
Валидация структуры ответа
Безопасный unwrap конверта
async function safeApiCall(url) {
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Accept': 'application/json'
},
// Защита от медленных ответов
signal: AbortSignal.timeout(10000)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
// Проверяем структуру конверта
if (typeof data.status !== 'number' || !data.body) {
throw new Error('Invalid response structure');
}
// Проверяем бизнес-статус
if (data.status !== 200) {
const errorCode = data.body?.error?.code || 'UnknownError';
const errorMessage = data.body?.error?.message || 'API Error';
throw new Error(`API Error [${errorCode}]: ${errorMessage}`);
}
return data.body;
} catch (error) {
// Не показывайте технические детали пользователю
console.error('API call failed:', error);
// Покажите дружелюбное сообщение
throw new Error('Не удалось загрузить данные. Попробуйте позже.');
}
}
Обработка ошибок для пользователя
Безопасный UI для ошибок
try {
const streams = await safeApiCall(apiUrl);
renderStreams(streams.availableStreams);
} catch (error) {
// ✅ Показываем понятное сообщение
showUserMessage('Не удалось загрузить трансляции');
// ✅ Логируем детали для разработчиков
console.error('Failed to fetch streams:', error);
// ❌ НЕ показывайте технические детали пользователю
// alert(JSON.stringify(error)); // Плохо!
}
🚨 Мониторинг и алертинг
Отслеживание аномалий
Настройте мониторинг для обнаружения проблем:
Мониторинг rate limits
let rateLimitHits = 0;
async function monitoredApiCall(url) {
try {
const response = await fetch(url);
if (response.status === 429) {
rateLimitHits++;
// Отправьте метрику в вашу систему мониторинга
trackMetric('api.rate_limit.hit', 1);
// Логируйте для анализа
const retryAfter = response.headers.get('Retry-After');
console.warn(`Rate limit hit. Retry after ${retryAfter}s`);
// Алерт при частых 429
if (rateLimitHits > 5) {
sendAlert('High rate limit hits detected');
}
}
return response;
} catch (error) {
trackMetric('api.error', 1, { error: error.name });
throw error;
}
}