Перейти к основному содержимому

Безопасность и лучшие практики

Этот раздел содержит рекомендации по безопасной интеграции 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; preload
  • X-Frame-Options: DENY или Content-Security-Policy: frame-ancestors 'none'
  • X-Content-Type-Options: nosniff
  • Referrer-Policy: no-referrer-when-downgrade
  • Permissions-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 автоматически экранирует

Валидация 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;
}
}

Метрики для отслеживания

МетрикаКогда алертитьДействие
Доля 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 разрешен только для нужных доменов

Пример безопасного клиента

TypeScript безопасный wrapper
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:

  1. НЕ публикуйте детали в публичных issue трекерах
  2. Отправьте email на security@shopstory.live
  3. Укажите:
    • Описание уязвимости
    • Шаги для воспроизведения
    • Потенциальное влияние
    • Ваши контакты для связи

Мы ценим ответственное раскрытие информации и ответим в течение 48 часов.


🎓 Дополнительные ресурсы


Следование этим рекомендациям поможет обеспечить безопасную интеграцию ShopStory API в ваше приложение.