Безопасность и лучшие практики
Этот раздел содержит рекомендации по безопасной интеграции ShopStory Public API в ваше приложение.
🔐 Защита applicationId
Не публикуйте учетные данные
Ваш applicationId — это идентификатор для доступа к API. Защищайте его:
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
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 и применить дополнительные проверки.
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:
<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';">
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:
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
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();
}
📊 Безопасная обработка ответов
Валидация структуры ответа
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('Не удалось загрузить данные. Попробуйте позже.');
}
}
Обработка ошибок для пользователя
try {
const streams = await safeApiCall(apiUrl);
renderStreams(streams.availableStreams);
} catch (error) {
// ✅ Показываем понятное сообщение
showUserMessage('Не удалось загрузить трансляции');
// ✅ Логируем детали для разработчиков
console.error('Failed to fetch streams:', error);
// ❌ НЕ показывайте технические детали пользователю
// alert(JSON.stringify(error)); // Плохо!
}
🚨 Мониторинг и алертинг
Отслеживание аномалий
Настройте мониторинг для обнаружения проблем:
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;
}
}
Метрики для отслеживания
| Метрика | Когда алертить | Действие |
|---|---|---|
| Доля 429 ответов | > 5% запросов | Увеличить паузы между запросами |
| Средняя задержка | > 2 секунд | Проверить сеть, добавить кэш |
| Доля ошибок 5xx | > 1% | Связаться с поддержкой ShopStory |
| Рост трафика | +300% за час | Проверить на утечку циклов |
🔄 Retry Policy
Экспоненциальный backoff
async function fetchWithRetry(url, maxRetries = 3) {
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url, {
signal: AbortSignal.timeout(10000)
});
// Успех - возвращаем сразу
if (response.ok) {
return await response.json();
}
// 429 - слишком много запросов
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
const delay = Math.min(retryAfter * 1000, 300000); // Max 5 минут
console.warn(`Rate limited. Waiting ${delay}ms before retry`);
await sleep(delay);
continue;
}
// 5xx - серверная ошибка, повторяем с backoff
if (response.status >= 500) {
const backoffDelay = Math.min(1000 * Math.pow(2, attempt), 32000);
console.warn(`Server error ${response.status}. Retry in ${backoffDelay}ms`);
await sleep(backoffDelay);
continue;
}
// 4xx - клиентская ошибка, не повторяем
throw new Error(`Client error: ${response.status}`);
} catch (error) {
lastError = error;
// Последняя попытка - выбрасываем ошибку
if (attempt === maxRetries - 1) {
throw lastError;
}
// Backoff для сетевых ошибок
const backoffDelay = Math.min(1000 * Math.pow(2, attempt), 32000);
await sleep(backoffDelay);
}
}
throw lastError;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
🔍 Тестирование безопасности
Контрольный чеклист перед prod
## Security Checklist
- [ ] applicationId хранится в переменных окружения
- [ ] Все входные параметры валидируются
- [ ] Используется HTTPS для всех запросов
- [ ] Таймауты настроены для всех fetch/axios вызовов
- [ ] Ошибки логируются, но не показываются пользователю
- [ ] CSP настроен для встраиваемых компонентов
- [ ] Нет XSS при рендеринге данных из API
- [ ] Rate limit retry policy реализована
- [ ] Мониторинг 429/5xx настроен
- [ ] Deep links валидируются перед использованием
- [ ] Нет sensitive данных в логах браузера
- [ ] CORS разрешен только для нужных доменов
Пример безопасного клиента
interface ApiResponse<T> {
status: number;
serverTime: string;
body: T | { error: { code: string; message: string } };
}
class ShopStoryClient {
private readonly applicationId: string;
private readonly baseUrl = 'https://app.shopstory.live';
private readonly timeout = 10000;
constructor(applicationId: string) {
if (!applicationId || !/^[a-zA-Z0-9_-]+$/.test(applicationId)) {
throw new Error('Invalid applicationId');
}
this.applicationId = applicationId;
}
private validateNumber(value: unknown, min: number, max: number, defaultValue: number): number {
const num = parseInt(String(value));
if (isNaN(num)) return defaultValue;
return Math.max(min, Math.min(max, num));
}
async getStreams(options: {
limit?: number;
offset?: number;
feedProductId?: string;
} = {}) {
const limit = this.validateNumber(options.limit, 1, 50, 10);
const offset = this.validateNumber(options.offset, 0, Infinity, 0);
const params = new URLSearchParams({
applicationId: this.applicationId,
limit: limit.toString(),
offset: offset.toString()
});
if (options.feedProductId) {
if (!/^[a-zA-Z0-9_-]+$/.test(options.feedProductId)) {
throw new Error('Invalid feedProductId format');
}
params.set('feedProductId', options.feedProductId);
}
const url = `${this.baseUrl}/v3/streams?${params}`;
try {
const response = await fetch(url, {
method: 'GET',
headers: { 'Accept': 'application/json' },
signal: AbortSignal.timeout(this.timeout)
});
const data: ApiResponse<any> = await response.json();
if (data.status !== 200) {
const error = (data.body as any)?.error;
throw new Error(`API Error: ${error?.message || 'Unknown error'}`);
}
return data.body;
} catch (error) {
console.error('ShopStory API error:', error);
throw new Error('Failed to fetch streams');
}
}
}
// Использование
const client = new ShopStoryClient(process.env.SHOPSTORY_APP_ID!);
const streams = await client.getStreams({ limit: 20, offset: 0 });
📞 Сообщить о проблеме безопасности
Если вы обнаружили уязвимость в ShopStory API:
- НЕ публикуйте детали в публичных issue трекерах
- Отправьте email на security@shopstory.live
- Укажите:
- Описание уязвимости
- Шаги для воспроизведения
- Потенциальное влияние
- Ваши контакты для связи
Мы ценим ответственное раскрытие информации и ответим в течение 48 часов.
🎓 Дополнительные ресурсы
- OWASP API Security Top 10
- Content Security Policy Guide
- Rate limiting best practices
- Caching strategies
Следование этим рекомендациям поможет обеспечить безопасную интеграцию ShopStory API в ваше приложение.