fix: индекси за листовите сортове + екраниране на JSON-LD (перформанс + hardening)#212
fix: индекси за листовите сортове + екраниране на JSON-LD (перформанс + hardening)#212B353N wants to merge 3 commits into
Conversation
The list pages keyset-paginate with ORDER BY <sortExpr> <dir>, <id> <dir> LIMIT N.
Six user-selectable sorts had no matching index, so the planner fell back to a
full table SCAN + temp-B-tree ORDER BY on every page (D1 bills rows scanned):
/contracts date-desc, date-asc (idx_contracts_signed is on the bare column,
not the COALESCE(signed_at, ...) expr the query uses)
/companies count, authorities
/authorities count, avg
Add one index per missing sort, matching the exact ORDER BY expression plus the
keyset id tiebreak, so SQLite walks the index and stops at LIMIT. Additive,
idempotent; rollup tables are DELETE+INSERT-refreshed so the indexes survive ships.
A sqlite3 EXPLAIN QUERY PLAN test proves each sort full-scans before and index-walks
after.
root.tsx embeds JSON-LD via dangerouslySetInnerHTML with a raw JSON.stringify. JSON.stringify does not escape '<', so a '</script>' in any string value would close the <script> element early (stored XSS) — the exact sink the project's own review standard (docs/review-security.md) requires be escaped. Today only the request origin reaches the graph (new URL() cannot make it carry '</script>'), so this is not currently exploitable; the jsonLdScript helper closes the sink pre-emptively for any DB/user-derived field added later. A unit test proves '<' is escaped, U+2028/U+2029 are escaped, and the output stays JSON-equivalent.
|
Благодаря за прегледната и добре документирана заявка. Прочетох целия diff, PR описанието, двата коммита и локално сверих индексите спрямо реалния код на заявките, keyset строителя, съществуващите миграции и XSS sink-а. По-долу е резултатът. ОбхватДва доказуеми hardening-a в един бранч (заменя #211):
Сигурност и целостност на данните — чистоSQL injection — няма. Сортовете минават през allowlist преди да достигнат SQL:
Индексите съвпадат байт-по-байт с емитирания
XSS sink — коректно затворен. Проверих и за backdoor/обфускация/промяна на URL-и/нови зависимости — няма. Миграцията е адитивна и идемпотентна ( Дребни бележки (не блокират)
Нищо от горното не е дефект в самата промяна. Вердикт: Approve (одобрявам на същество) — сигурност и целостност на данните чисти, OWASP-съвместимо; преди мердж уредете само номерацията на миграцията спрямо #210 и потвърдете |
|
Одобрявам ( Бележка (cross-PR): този PR добавя |
nedda76
left a comment
There was a problem hiding this comment.
Прегледах двете части — чисто.
A. JS екраниране (XSS). jsonLdScript е коректен фикс: екранира всяко < като < (обезврежда </script, <!-- и double-escape странностите) + U+2028/U+2029, и остава JSON-еквивалентно. Приложен е върху единствения inline-script sink в приложението (JSON-LD острова в root.tsx); всичко останало минава през auto-escape на React. Няма останал reflected/stored XSS път — включително през атакуемо DB поле (напр. фирма, регистрирана с <script в името).
B. Индекси. И шестте нови индекса точно съвпадат с ORDER BY <expr> <dir>, <id> <dir>, който keyset пейджърът издава (вкл. COALESCE(signed_at,'') изразите — затова не дублират заварения idx_contracts_signed); никой не дублира съществуващ индекс; migration-ът е нов, адитивен, идемпотентен (CREATE INDEX IF NOT EXISTS). Уговорка: индексите ускоряват само нефилтрирания списък — при активни филтри source() минава към derived GROUP BY, който не може да ги ползва. Присъщо на подхода; hot path-ът (списък по подразбиране) е покрит правилно.
Координация: #212 и #210 добавят по един 0002_* migration — който влезе втори, трябва да се преномерира на 0003, иначе един от двата тихо няма да се приложи.
Консолидира двете доказуеми находки от прегледите (перформанс + сигурност) в един бранч. Заменя #211.
1. perf(db): индекси за подредбата на неполагащите листови сортове
Листовете странират с keyset
ORDER BY <израз> <посока>, <id> <посока> LIMIT N. Дефолтните сортове имат съвпадащ индекс; шест избираеми сорта нямаха - планировчикът правешеSCAN <table>+USE TEMP B-TREE FOR ORDER BY, т.е. сканираше и сортираше цялата таблица предиLIMITна всяка страница (D1 таксува сканираните редове)./contractsdate-desc,date-ascidx_contracts_signedе на голотоsigned_at, а заявката подрежда поCOALESCE(signed_at, …)/companiescount,authorities/authoritiescount,avgМиграция
0002_list_sort_indexes.sqlдобавя по един индекс, съвпадащ с точнияORDER BYизраз + keyset id tiebreak. Адитивна, идемпотентна; rollup-ите се опресняват сDELETE+INSERT, затова индексите преживяват ship. Измерено при 200k договора: date-desc пада от ~0.30s на <10ms.Съзнателно изключени (доказано, че не си струват): id-tiebreak на дефолтните индекси - бенчмаркът показа 0 измерима полза (single-col вече е ~0ms дори при много еднакви стойности), само излишен write-cost при refresh.
ANALYZE- показано, че може да регресира/contractsvalue-desc доSCAN authorities.2. fix(web): екраниране на
<в JSON-LD data island-а (defense-in-depth)root.tsx слага JSON-LD през
dangerouslySetInnerHTMLсъс суровJSON.stringify.JSON.stringifyне екранира<, така че</script>в която и да е стойност би затворил<script>елемента рано (stored XSS) - точно sink-ът, който собственият ви стандарт (docs/review-security.md) изисква да е екраниран.Не е експлоатируемо днес (единствената вкарана стойност е
origin, аnew URL()хвърля при host с</script>), затова е defense-in-depth: новиятjsonLdScripthelper затваря sink-а превантивно за всяко бъдещо поле от базата/потребителя в графа. Екранира<→<(JSON-еквивалентно) + U+2028/U+2029.Доказателство (тестове)
packages/db/src/list-sort-indexes.test.ts- прилага миграциите на реален sqlite3 и за всеки от 6-те сорта проверяваEXPLAIN QUERY PLAN: ПРЕДИ0002планът имаUSE TEMP B-TREE FOR ORDER BY(дефектът), СЛЕД - върви по новия индекс без sort step.apps/web/app/lib/json-ld.test.ts-<се екранира (няма</script>breakout), U+2028/2029 се екранират, изходът остава JSON-еквивалентен.Вид промяна
perf— индекси (без промяна в резултатите на заявките)fix— security hardening (JSON-LD sink)Как е тествано
pnpm typecheck- минава.pnpm test- минава:@sigma/web339 (вкл. 4 нови JSON-LD),@sigma/db195 (вкл. 12 нови plan теста).pnpm lint- чисто.Чеклист
Co-Authored-By:midt-bg/sigma:mainIF NOT EXISTS).env*/.dev.varsБележки за координация
0002; PR feat(web): „Подобни договори" - ценови ориентир по CPV кохорта на страницата на договора #210 (feat/similar-contracts) също въвежда0002_*. Който се мерджне втори, преномерира на0003.