Verzije PDF Library for Delphi (PDFlibPas) pre v3.539.47 mogle su da dekodiraju escapovani tekst dvaput pri crtanju HTML-a ili Markdown-a u PDF. DrawHTMLText i DrawHTMLTextBox parsiraju HTML, normalizuju ga nazad u HTML, pa ga ponovo parsiraju, tako da je tekst upisan kao <unsafe> stizao do drugog parsiranja kao pravi tag. Od v3.539.47 svaki entitet dekodira se tačno jednom, a tekst se ponovo escapuje svuda gde se vraća u HTML
Scenario koji ovo izlaže je sasvim običan. Help desk izvozi tikete u PDF, i komentar kupca ide u HTML šablon. Programer je uradio pravu stvar i escapovao komentar, pa je <b> postao <b>. Unutar renderer-a to escapovanje tiho je poništeno: komentar je izašao boldovan, nepoznato ime taga jednostavno je nestalo sa stranice, a escapovan anchor pretvoren je u klikabilnu anotaciju linka. Bez izuzetka, bez upozorenja, sasvim validan PDF koji govori nešto drugačije od podataka
Zašto escapovani tekst postaje pravi tag u PDF-u?
Escapovani tekst postao je markup zato što renderer izvodi dva prolaza parsiranja, a korak normalizacije između njih upisivao je već dekodirani tekst nazad u HTML bez ponovnog escapovanja. Svako dekodiranje koje je prvi prolaz izveo bilo je tada dostupno drugom prolazu kao živa sintaksa
Ta dva prolaza postoje s dobrim razlogom. Prvi prolaz gradi listu elemenata tagova i reči. NormalizeParsedHTML zatim razrešuje kaskadu stylesheet-a: pravila iz <style> blokova poklapa sa svakim tagom, spaja ih sa inline style atributima, čuva rezultat na tagu i serijalizuje celu listu elemenata nazad u HTML string. Prolaz rasporeda parsira taj normalizovani string. To je ista mašinerija koja pogoni flexbox, CSS grid i footnote raspored u PDFlibPas HTML renderovanju
Mana je bila u tome kako su se reči serijalizovale. Tagovi upisivani su nazad u svom izvornom obliku, dok su reči upisivane u dekodiranom obliku. Reč koju je prvi prolaz dekodirao iz <unsafe> u <unsafe> završila je u normalizovanom HTML-u kao sirove uglaste zagrade, a drugi prolaz pročitao ju je kao element. Oko te glavne greške stajala su tri manja curenja koja su ukazivala istim smerom:
&nije bio u podržanom skupu entiteta, pa seR&Dispisivao bukvalno i nije bilo načina da se kao tekst napiše bukvalni zapis entiteta poput<- Faza crtanja zamenjivala je
drugi put, pošto je parsiranje već bilo gotovo, pa bukvalni zapis entiteta mogao je i dalje nestati na samom kraju - Markdown escapovanje koda preskakalo je ampersand, a dataset eksporter escapovao je samo uglaste zagrade, pa su zapisi entiteta unutar koda ili vrednosti ćelija dekodirani kao markup
| Ulaz koji stiže renderer-u | Pre v3.539.47 | Od v3.539.47 |
|---|---|---|
<unsafe> | Parsiran kao tag, tekst nikada ne stiže do stranice | <unsafe> ispisan kao tekst |
<b>x</b> | x ispisan boldovano | <b>x</b> ispisan kao tekst |
R&D | R&D ispisan bukvalno | R&D |
&lt; | &lt; ispisan bukvalno | < |
Markdown code span koji sadrži | Postao neprelomni razmak | ispisan kao tekst |
Vrednost ćelije dataset-a < | < | < |
Kako v3.539.47 čini dekodiranje HTML entiteta jednoprolaznim
PDFlibPas v3.539.47 čini dekodiranje entiteta jednoprolaznim sa tri usklađene izmene: parser dekodira & poslednji, faza crtanja više ništa ne dekodira, i svako mesto koje vraća dekodirane reči u HTML prvo ih ponovo escapuje
Podržani skup entiteta za tekstualni sadržaj sada je <, >, & i . Sve ostalo, uključujući numeričke reference poput A i imenovane entitete poput ", ostaje bukvalni tekst. Ta granica je bitna za to kako ćete escapovati svoj unos, kao što je prikazano ispod
Redosled unutar dekodera je prva popravka. Da je & dekodiran prvi, ulaz &lt; postao bi < i sledeća zamena pretvorila bi ga u <, dvostruko dekodiranje koje se dešava unutar jednog prolaza. ANSI putanja reči zato zamenjuje <, > i prve, a & poslednji, pa ampersand koji proizvede nikada više nije ispitivan. UTF-16 putanja reči je jedno skeniranje s leva na desno u koracima od dva bajta koje svaki pogodak prepisuje u mestu i prelazi preko njega, što istu garanciju daje po strukturi
Druga popravka uklanja kasnu zamenu iz faze crtanja. Dekodiranje pripada parseru i nikome drugom, pa je reč koja stigne do line breaker-a konačan tekst
Treća popravka je pravilo granice. NormalizeParsedHTML sada escapuje &, < i > u svakoj dekodiranoj reči pre nego što je doda normalizovanom HTML-u. Drugi prolaz dekodira ga nazad u tačno isti tekst, pa je net efekat preko celog cevovoda jedno dekodiranje. Nastavni string prati isto pravilo: reči koje nisu stale u kutiju escapuju se pre nego što se dodaju LeftOverText-u, a ostatak ostatka kopira se iz normalizovanog HTML-a, koji je već u escapovanom obliku. Petlja koja skuplja te zaostale reči sada je ograničena i brojem reči, dok je stara repeat petlja mogla preći preko poslednje reči
Zašto UTF-16BE escapovanje ne može da koristi zamenu na nivou bajtova?
UTF-16BE escapovanje ne može da koristi zamenu na nivou bajtova jer se dvobajtni obrazac za ampersand može protegnuti preko dva nepovezana karaktera. Jedina ispravna jedinica rada je cela 16-bitna code jedinica
Renderer čuva Unicode reči kao big-endian UTF-16 spakovane u bajt stringove, visoki bajt prvi. Ampersand je 00 26. Uzmite sada U+0100 (latinično veliko A sa makronom, bajtovi 01 00) a iza njega U+2603 (snegovec, bajtovi 26 03). Bajt sekvenca je 01 00 26 03, i bajtovi dva i tri čitaju se kao 00 26. Bajt pretraga za #0'&' nalazi ampersand koji ne postoji, ubacuje bajtove za & u sredinu dva karaktera i pomera svaki sledeći karakter za jedan bajt
To nije egzotičan ugaoni slučaj. Svaki karakter čiji je niski bajt nula može dati prvu polovinu; U+4E00, jedan od najčešćih CJK ideograma, kvalifikuje se. Uglaste zagrade imaju istu izloženost: 00 3C i 00 3E pojavljuju se kad god takav karakter prati jedan od U+3C00 do U+3EFF u CJK Extension A. Popravka u EscapeHTMLWord raspakuje bajtove u WideString, escapuje karakter po karakter i ponovo spakuje rezultat. Dekoderska strana već je bila bezbedna jer ispituje obrasce samo na parnim granicama code jedinica
Isto pravilo važi za vaš sopstveni kod. Ako ikada držite UTF-16 tekst kao TBytes, na primer posle TEncoding.BigEndianUnicode.GetBytes, ne pretražujte ga za bajt obrasce. Konvertujte nazad u string i radite na karakterima
Markdown kod blokovi i dataset izvozi: escapujte ampersand prvi
Od v3.539.47 oba HTML proizvođača unutar PDFlibPas-a, Markdown konverter i dataset eksporter, escapuju ampersand pre uglastih zagrada, pa jedno dekodiranje u renderer-u vraća tačno originalni tekst
U MarkdownToHTML, inline code spanovi i fenced ili uvučeni kod blokovi sada mapiraju & u &, < u < i > u >, dok razmaci postaju a tab postaje četiri njih da bi uvlačenje ostalo. Obična Markdown proza escapuje samo uglaste zagrade, pa sirovi HTML u prozi ne može da ubaci tagove, dok autor i dalje može namerno da napiše &, otprilike kako Markdown autori i očekuju. DrawMarkdownText i DrawMarkdownTextBox koriste istu konverziju, pa se kod u PDF-u prikazuje tačno kako je ukucan:
uses
System.SysUtils, PDFlibrary;
procedure RenderCodeSample;
var
Lib: TPDFlib;
Md, Html: WideString;
begin
Md := 'Comparison helper:' + sLineBreak + sLineBreak +
'```' + sLineBreak +
'if (A < B) and (Flags <> 0) then' + sLineBreak +
' WriteLn(''<tag> & R&D'');' + sLineBreak +
'```';
Lib := TPDFlib.Create;
try
// Pregledajte HTML: u kodu, '&' postaje '&' a '<' postaje '<'
Html := Lib.MarkdownToHTML(Md);
Lib.SetOrigin(1); // početak gore-levo, Y raste naniže
Lib.SetMeasurementUnits(0); // tačke
// Stranica prikazuje kod tačno kako je ukucan, zapisi entiteta uključeno
Lib.DrawMarkdownText(50, 50, 495, Md);
Lib.SaveToFile('code-sample.pdf');
finally
Lib.Free;
end;
end;
Dataset eksporter je poučan slučaj. Pre v3.539.47 escapovao je samo uglaste zagrade, i to namerno: renderer nije dekodirao &, pa bi escapovanje ampersanda ispisalo & u svakoj ćeliji koja ga sadrži. Zaobilazno rešenje bilo je ispravno za stari renderer i pogrešno uopšte, jer je vrednost ćelije koja slučajno sadrži < dekodirana u <. Sa popravljenim renderer-om, eksporter escapuje & prvi, i vrednost poput R&D < & završava u PDF-u bukvalno. Ako tako gradite izveštaje, uputstvo na izvozu TDataSet-a u PDF izveštaj u Delphi-ju pokriva ostatak eksportera
Zašto ampersand mora prvi vredi jednom izgovoriti. Escapujte < prvi i dobijete <; escapujte & drugi i to postaje &lt;, što ispravno jedno dekodiranje prikazuje kao < umesto <. Sekvencijalan lanac zamena ispravan je samo kad je escape karakter sam obrađen pre bilo čega što ga uvodi
Kako da escapujete nepouzdani tekst za DrawHTMLTextBox?
Za PDFlibPas HTML renderovanje, escapujte nepouzdani tekstualni sadržaj zamenom &, pa <, pa >, tačno jednom, i držite nepouzdane podatke potpuno van vrednosti atributa
uses
System.SysUtils, PDFlibrary;
// Escapuje nepouzdani tekst za PDFlibPas HTML tekstualni sadržaj.
// '&' mora biti zamenjen prvi, inače bi ampersand unutar
// već proizvedenog '<' bio escapovan drugi put
function EscapeHTMLText(const S: string): string;
begin
Result := StringReplace(S, '&', '&', [rfReplaceAll]);
Result := StringReplace(Result, '<', '<', [rfReplaceAll]);
Result := StringReplace(Result, '>', '>', [rfReplaceAll]);
end;
procedure RenderTicket(const CustomerComment: string);
var
Lib: TPDFlib;
Html: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.SetOrigin(1);
Lib.SetMeasurementUnits(0);
Html := '<p><b>Customer comment</b></p>' +
'<p>' + EscapeHTMLText(CustomerComment) + '</p>';
Lib.DrawHTMLText(50, 50, 495, Html);
Lib.SaveToFile('ticket.pdf');
finally
Lib.Free;
end;
end;
Na v3.539.47 komentar poput Try <a href="https://example.com">this</a> & <b> pojavljuje se na stranici karakter po karakter. Pre v3.539.47 isti escapovani unos mogao je proizvesti živu anotaciju linka, što je deo koji od greške prikaza pravi bezbednosni problem: komentar tiketa nikada ne bi trebalo da može da zasede klikabilni URL u dokumentu kojem vaše osoblje veruje
Primetite šta funkcija ne escapuje. Opšte-nameni HTML escaperi konvertuju i " u " i ' u ', što je ispravno za pregledač. PDFlibPas dekodiranje teksta prepoznaje samo četiri ranije navedena entiteta, pa bi se ta dva ispisala bukvalno kao " i '. Znaci navoda su bezopasni u tekstualnom sadržaju; bitni su samo unutar vrednosti atributa, a renderer uopšte ne dekodira entitete u atributima. Bezbedan dizajn zato nije bolji escaper nego pravilo: nepouzdani podaci nikada ne idu u href, src ili style. Ako cilj veze zaista mora da dođe iz korisničkih podataka, validirajte ga sami protiv bele liste šema i karaktera i odbijte sve što sadrži znake navoda ili uglaste zagrade
Dve napomene o nadogradnji prate direktno iz popravke:
- Ako je vaš kod prestao da escapuje
&jer su starije verzije ispisivale&bukvalno, vratite to. Bez toga, korisnički tekst koji sadrži<sada se prikazuje kao<, i dalje bezopasan tekst ali više nije ono što je korisnik ukucao - Ne escapujte dvaput. Tekst koji prođe kroz dva escapera renderuje
<kao vidljivi zapis<, pa pronađite jedinu granicu na kojoj vaši podaci ulaze u HTML i escapujte samo tamo
Paginacija sa LeftOverText bez lomljenja escapovanja
DrawHTMLTextBox vraća HTML koji nije stao, obično nazvan LeftOverText, i od v3.539.47 taj ostatak čuva bukvalne zapise entiteta i escapovane uglaste zagrade kad ga prosledite sledećoj kutiji. Pravilo za pozivaoce je jednostavno: prosledite ga nazad nepromenjen
const
BoxLeft = 50;
BoxTop = 50;
BoxWidth = 495; // dimenzionisano za A4 stranicu u tačkama
BoxHeight = 740;
MaxPages = 500;
procedure RenderLongHTML(Lib: TPDFlib; const Html: WideString);
var
Rest: WideString;
Pages: Integer;
begin
Lib.SetOrigin(1);
Lib.SetMeasurementUnits(0);
Rest := Lib.DrawHTMLTextBox(BoxLeft, BoxTop, BoxWidth, BoxHeight, Html);
Pages := 1;
while (Rest <> '') and (Pages < MaxPages) do
begin
Lib.NewPage;
Inc(Pages);
// LeftOverText je već escapovan HTML motora: nikada ga ne escapujte ni ne de-eskapujte
Rest := Lib.DrawHTMLTextBox(BoxLeft, BoxTop, BoxWidth, BoxHeight, Rest);
end;
if Rest <> '' then
raise Exception.CreateFmt('Content still left after %d pages', [MaxPages]);
end;
Tretirajte ostatak kao neproziran. To je normalizovani HTML motora, sa već razrešenim stilovima, pa ga ne provlačite kroz svoj escaper, ne dekodirajte ga i ne ušivajte korisnički tekst u njega. Ograničenje stranica je jeftino osiguranje: ako neki element nikada ne može da stane u kutiju, neograničena petlja nema prirodan izlaz
Markdown ima svoj nastavak. DrawMarkdownTextBox vraća token koji počinje internim markerom da bi sledeći poziv mogao da preskoči konverziju; vratite ga DrawMarkdownTextBox ili DrawMarkdownText-u, a ne HTML ulaznim tačkama, koje bi marker iscrtao kao tekst
Opšta lekcija: dekodiraj jednom, ponovo kodiraj na svakoj granici
Svaki cevovod koji parsira tekst, serijalizuje rezultat nazad u istu sintaksu i ponovo ga parsira mora dekodiranje da tretira kao operaciju koja se dešava na tačno jednom mestu, i mora ponovo kodirati na svakoj granici gde dekodirani tekst ponovo postaje sintaksa. Template motori, HTML sanitajzeri i Markdown-to-HTML-to-PDF lanci dele ovaj oblik i padaju na isti način kad serijalizator zaboravi da proizvodi markup
Simptomi su predvidivi kad jednom znate oblik. Premalo ponovnog kodiranja pretvara podatke u sintaksu, što je pravac injekcije. Previše kodiranja, ili dekoder koji radi dvaput, pokazuje čitaocu zapise entiteta ili ih jede, što je pravac prikaza. Popraviti samo jedan pravac obično polomi drugi, pa je PDFlibPas popravka morala da doda & dekodiranje, da ga preuredi, da ukloni kasno dekodiranje i doda ponovno escapovanje u istom izdanju. Isti princip teče i obrnuto kad se PDF sadržaj izvozi kao strukturirani tekst, kao u PDF to Markdown i DOCX semantičkom izvozu iz Delphi-ja, gde svaki bukvalni karakter mora biti escapovan za ciljnu sintaksu tačno jednom
Brza referentna provera
- Nadogradite na PDFlibPas v3.539.47 ili noviji ako renderujete HTML ili Markdown koji sadrži korisničke podatke
- Escapujte tekstualni sadržaj sa
&prvi, pa<i>; ne konvertujte znake navoda za PDFlibPas tekst - Escapujte jednom, na jedinom mestu gde podaci ulaze u HTML string
- Držite nepouzdane vrednosti van
href,srcistyle, ili ih validirajte protiv bele liste - Očekujte da se u tekstu dekodiraju samo
<,>,&i ; drugi entiteti ostaju bukvalni - Prosledite
LeftOverTextnazadDrawHTMLTextBox-u nepromenjen i ograničite petlju stranica - Prosledite Markdown nastavne tokene samo
DrawMarkdownTextBoxiliDrawMarkdownText-u - Nikada ne pretražujte UTF-16 bajt bufere za bajt obrasce; radite na celim code jedinicama
HTML i Markdown renderovanje, izvoz dataset izveštaja i ostatak layout motora isporučuju se u izvornom Pascal kodu PDF Library for Delphi, za Delphi i Free Pascal. Pogledajte stranicu proizvoda PDFlibPas za izdanja, podršku platformi i trial preuzimanje