Verzije PDF Library for Delphi (PDFlibPas) prije v3.539.47 mogle su escapirani tekst dekodirati dvaput pri crtanju HTML-a ili Markdowna u PDF. DrawHTMLText i DrawHTMLTextBox parsiraju HTML, normaliziraju ga natrag u HTML, pa ga ponovno parsiraju, pa je tekst zapisan kao <unsafe> došao do drugog parsiranja kao pravi tag. Od v3.539.47 svaki se entitet dekodira točno jednom, a tekst se ponovno escapira gdje god se vraća u HTML
Scenarij koji to izlaže svakidašnji je. Help desk izvozi tickete u PDF, a komentar kupca završi u HTML predlošku. Programer je učinio pravu stvar i escapirao komentar, pa je <b> postao <b>. Unutar renderera to escapiranje tiho je poništeno: komentar je izašao boldan, nepoznato ime taga jednostavno je nestalo sa stranice, a escapirani anchor pretvorio se u klikabilu anotaciju poveznice. Bez iznimke, bez upozorenja, sasvim valjan PDF koji govori nešto drugačije od podataka
Zašto escapirani tekst postaje pravi tag u PDF-u?
Escapirani tekst postao je markup jer renderer izvodi dva pars prolaza, a korak normalizacije između njih zapisao je već dekodirani tekst natrag u HTML bez ponovnog escapiranja. Svako dekodiranje koje je izvršilo prvo parsiranje tada je bilo dostupno drugom parsiranju kao živa sintaksa
Ta dva prolaza postoje s dobrim razlogom. Prvo parsiranje gradi listu elemenata tagova i riječi. NormalizeParsedHTML zatim razrješuje kaskadu stylesheeta: pravila iz <style> blokova uspoređuje sa svakim tagom, spaja ih s inline style atributima, rezultat sprema na tag i cijelu listu elemenata serijalizira natrag u HTML string. Prolaz rasporeda parsira taj normalizirani string. To je isti mehanizam koji pogoni flexbox, CSS grid i raspored fusnota u PDFlibPas HTML renderiranju
Manjkavost je bila u tome kako su riječi serijalizirane. Tagovi zapisivani su natrag u svom izvornom obliku, dok su riječi zapisivane u dekodiranom obliku. Riječ koju je prvo parsiranje dekodiralo iz <unsafe> u <unsafe> dospjela je u normalizirani HTML kao sirovi uglate zagrade, a drugo je parsiranje pročitalo kao element. Oko te glavne greške sjedila su tri manja curenja koja su pokazivala istim smjerom:
&nije bio u podržanom skupu entiteta, pa seR&Dispisivao doslovno i nije bilo načina da se doslovni zapis entiteta poput<napiše kao tekst- Faza crtanja zamjenjivala je
drugi put, nakon što je parsiranje već bilo gotovo, pa je doslovni zapis entiteta mogao još nestati na samom kraju - Escapiranje Markdown koda preskakalo je ampersand, a izvoznik skupa podataka escapirao je samo uglate zagrade, pa su zapisi entiteta unutar koda ili vrijednosti ćelija dekodirani kao markup
| Ulaz koji dolazi do renderera | Prije v3.539.47 | Od v3.539.47 |
|---|---|---|
<unsafe> | Parsiran kao tag, tekst nikad ne doseže stranicu | <unsafe> nacrtan kao tekst |
<b>x</b> | x nacrtan boldano | <b>x</b> nacrtan kao tekst |
R&D | R&D ispisano doslovno | R&D |
&lt; | &lt; ispisano doslovno | < |
Markdown code span koji sadrži | Postao neprelomivi razmak | nacrtan kao tekst |
Vrijednost ćelije skupa podataka < | < | < |
Kako v3.539.47 svodi dekodiranje HTML entiteta na jedan prolaz
PDFlibPas v3.539.47 svodi dekodiranje entiteta na jedan prolaz s tri koordinirane izmjene: parser dekodira & posljednji, faza crtanja više ništa ne dekodira, a svako mjesto koje dekodirane riječi vraća u HTML najprije ih ponovno escapira
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 doslovan tekst. Ta granica važna je za to kako escapirate vlastiti ulaz, kao što je dolje prikazano
Redoslijed unutar dekodera prvi je popravak. Da je & dekodiran prvi, ulaz &lt; postao bi <, a sljedeća bi ga zamjena pretvorila u <, dvostruko dekodiranje unutar jednog prolaza. ANSI put riječi stoga najprije zamjenjuje <, > i , a & posljednji, pa ampersand koji proizvede nikad više nije ispitan. UTF-16 put riječi jest jedno skeniranje s lijeva na desno u dvobajtnim koracima koje svako podudaranje prepisuje na mjestu i prelazi preko njega, što istu garanciju daje strukturalno
Drugi popravak uklanja kasnu zamjenu iz faze crtanja. Dekodiranje pripada parseru i nikome drugome, pa je riječ koja dođe do lomitelja linija konačni tekst
Treći popravak jest pravilo granice. NormalizeParsedHTML sada escapira &, < i > u svakoj dekodiranoj riječi prije nego što je doda normaliziranom HTML-u. Drugo parsiranje dekodira je natrag u potpuno isti tekst, pa je neto učinak preko cijelog pipelinea jedno dekodiranje. Nastavni string slijedi isto pravilo: riječi koje se nisu uklopile u okvir escapiraju se prije nego što se dodaju u LeftOverText, a ostatak ostatka kopira se iz normaliziranog HTML-a, koji je već u escapiranom obliku. Petlja koja skuplja te zaostale riječi sada je omeđena i brojem riječi, dok je stara repeat petlja mogla koraknuti iza posljednje riječi
Zašto UTF-16BE escapiranje ne može koristiti zamjenu na razini bajtova?
UTF-16BE escapiranje ne može koristiti zamjenu na razini bajtova jer dvobajtni uzorak za ampersand može zahvatiti dva nepovezana znaka. Jedina ispravna jedinica rada jest cijela 16-bitna kodna jedinica
Renderer pohranjuje Unicode riječi kao big-endian UTF-16 spakiran u bajtne stringove, visoki bajt prvi. Ampersand je 00 26. Uzmite sada U+0100 (latinično veliko A s makronom, bajtovi 01 00) pa U+2603 (snjegović, bajtovi 26 03). Bajtni niz jest 01 00 26 03, a bajtovi dva i tri čitaju se 00 26. Bajtna pretraga za #0'&' pronaći će ampersand koji ne postoji, ušiti bajtove za & u sredinu dva znaka i odrezati svaki sljedeći znak za jedan bajt
To nije egzotični rubni slučaj. Svaki znak čiji je niski bajt nula može dati prvu polovicu; U+4E00, jedan od najčešćih CJK ideograma, kvalificira se. Uglate zagrade imaju istu izloženost: 00 3C i 00 3E pojavljuju se svaki put kad takvom znaku slijedi jedan od U+3C00 do U+3EFF u CJK Extension A. Popravak u EscapeHTMLWord raspakira bajtove u WideString, escapira znak po znak i rezultat ponovno pakira. Strana dekodera bila je već sigurna jer testira uzorke samo na parnim granicama kodnih jedinica
Isto pravilo vrijedi i za vaš vlastiti kod. Ako ikad držite UTF-16 tekst kao TBytes, na primjer nakon TEncoding.BigEndianUnicode.GetBytes, ne pretražujte ga za bajtnim uzorcima. Pretvorite natrag u string i radite na znakovima
Markdown kodni blokovi i izvozi skupa podataka: escapirajte prvo ampersand
Od v3.539.47 oba HTML proizvođača unutar PDFlibPasa, Markdown pretvarač i izvoznik skupa podataka, escapiraju ampersand prije uglatih zagrada, pa jedno dekodiranje u rendereru vraća točno izvorni tekst
U MarkdownToHTML, inline code spanovi i fenced ili uvučeni kodni blokovi sada mapiraju & u &, < u < i > u >, dok razmaci postaju , a tab četiri od njih da zadrži uvlačenje. Obična Markdown proza escapira samo uglate zagrade, pa sirovi HTML u prozi ne može ubaciti tagove, a autor i dalje može napisati & namjerno, otprilike kako Markdown autori i očekuju. DrawMarkdownText i DrawMarkdownTextBox koriste istu pretvorbu, pa se kod u PDF-u prikazuje točno kako je utipkan:
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
// Pogledajte HTML: u kodu, '&' postaje '&' a '<' postaje '<'
Html := Lib.MarkdownToHTML(Md);
Lib.SetOrigin(1); // ishodište gore lijevo, Y raste prema dolje
Lib.SetMeasurementUnits(0); // točke
// Stranica prikazuje kod točno kako je utipkan, s doslovnim zapisima entiteta
Lib.DrawMarkdownText(50, 50, 495, Md);
Lib.SaveToFile('code-sample.pdf');
finally
Lib.Free;
end;
end;
Izvoznik skupa podataka je poučan slučaj. Prije v3.539.47 escapirao je samo uglate zagrade, i to namjerno: renderer nije dekodirao &, pa bi escapiranje ampersanda ispisalo & u svakoj ćeliji koja ga sadrži. Workaround bio je ispravan za stari renderer i pogrešan općenito, jer je vrijednost ćelije koja je slučajno sadržavala < bila dekodirana u <. S popravljenim rendererom izvoznik escapira & prvi, i vrijednost poput R&D < & završava u PDF-u doslovno. Ako tako gradite izvještaje, uputstvo na izvozu TDataSet-a u PDF izvještaj u Delphiju pokriva ostatak izvoznika
Zašto ampersand mora ići prvi vrijedi jednom reći. Escapirajte < prvi i dobijete <; escapirajte & drugi i to postaje &lt;, što ispravno jedno dekodiranje prikazuje kao < umjesto <. Sekvencijalni lanac zamjena ispravan je samo kad se sam escape znak obradi prije svega što ga uvodi
Kako escapirati nepouzdani tekst za DrawHTMLTextBox?
Za PDFlibPas HTML renderiranje escapirajte nepouzdani tekstualni sadržaj zamjenom &, zatim <, pa >, točno jednom, i držite nepouzdane podatke potpuno izvan vrijednosti atributa
uses
System.SysUtils, PDFlibrary;
// Escapira nepouzdani tekst za tekstualni sadržaj PDFlibPas HTML-a.
// '&' mora se zamijeniti prvi, inače bi ampersand unutar
// već proizvedenog '<' bio escapiran 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 znak po znak. Prije v3.539.47 isti escapirani ulaz mogao je proizvesti živu anotaciju poveznice, što je dio koji pretvara grešku prikaza u sigurnosni problem: komentar tiketa nikad ne smije moći zasaditi klikabilni URL u dokumentu kojem vaše osoblje vjeruje
Primijetite što funkcija ne escapira. Općenamjenski HTML escaperi pretvaraju i " u " te ' u ', što je ispravno za preglednik. PDFlibPas dekodiranje teksta prepoznaje samo četiri ranije navedena entiteta, pa bi se ta dva ispisala doslovno kao " i '. Navodnici su bezopasni u tekstualnom sadržaju; važe samo unutar vrijednosti atributa, a renderer entitete u atributima uopće ne dekodira. Siguran dizajn stoga nije bolji escaper nego pravilo: nepouzdani podaci nikad ne idu u href, src ni style. Ako cilj poveznice stvarno mora doći iz korisničkih podataka, sami ga validirajte protiv allow-liste shema i znakova i odbijte sve što sadrži navodnike ili uglate zagrade
Dvije napomene o nadogradnji slijede izravno iz popravka:
- Ako je vaš kod prestao escapirati
&jer su starije verzije ispisivale&doslovno, dodajte ga natrag. Bez njega se korisnički tekst koji sadrži<sada prikazuje kao<, još uvijek bezopasan tekst, ali više nije ono što je korisnik utipkao - Ne escapirajte dvaput. Tekst koji prođe kroz dva escapera renderira
<kao vidljivi zapis<, pa pronađite jedinu granicu na kojoj vaši podaci ulaze u HTML i escapirajte samo tamo
Paginacija s LeftOverTextom bez lomljenja escapiranja
DrawHTMLTextBox vraća HTML koji se nije uklopio, obično nazvan LeftOverText, i od v3.539.47 taj ostatak čuva doslovne zapise entiteta i escapirane uglate zagrade kad ga prenesete u sljedeći okvir. Pravilo za pozivatelje jednostavno je: prenesite ga natrag nepromijenjen
const
BoxLeft = 50;
BoxTop = 50;
BoxWidth = 495; // dimenzionirano za A4 stranicu u toč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 već je escapirani HTML mehanizma: nikad ga ne escapirajte ni ne de-escapirajte
Rest := Lib.DrawHTMLTextBox(BoxLeft, BoxTop, BoxWidth, BoxHeight, Rest);
end;
if Rest <> '' then
raise Exception.CreateFmt('Content still left after %d pages', [MaxPages]);
end;
Prema ostatku postupajte kao prema neprozirnom. To je normalizirani HTML mehanizma, s već razriješenim stilovima, pa ga ne provlačite kroz vlastiti escaper, ne dekodirajte ga i ne ušivajte u njega korisnički tekst. Ograničenje stranica jeftino je osiguranje: ako neki element nikad ne može stati u okvir, petlja bez ograničenja nema prirodnog izlaza
Markdown ima svoj nastavak. DrawMarkdownTextBox vraća token koji počinje internim markerom da sljedeći poziv može preskočiti pretvorbu; predajte ga natrag DrawMarkdownTextBoxu ili DrawMarkdownTextu, a ne HTML ulaznim točkama, koje bi marker nacrtale kao tekst
Općenita lekcija: dekodiraj jednom, ponovno kodiraj na svakoj granici
Svaki pipeline koji parsira tekst, serijalizira rezultat natrag u istu sintaksu i ponovno ga parsira mora dekodiranje tretirati kao operaciju koja se događa na točno jednom mjestu i mora ponovno kodirati na svakoj granici gdje dekodirani tekst opet postaje sintaksa. Template enginei, HTML sanitizeri i lanci Markdown-u-HTML-u-PDF dijele taj oblik i padaju na isti način kad serijalizator zaboravi da proizvodi markup
Simptomi su predvidivi jednom kad znate oblik. Premalo ponovnog kodiranja pretvara podatke u sintaksu, što je smjer injekcije. Previše kodiranja, ili dekoder koji izvodi dvaput, prikazuje čitatelju zapise entiteta ili ih poždere, što je smjer prikaza. Popraviti samo jedan smjer obično slomi drugi, pa je PDFlibPas popravak morao u istom izdanju dodati dekodiranje &u, promijeniti mu redoslijed, ukloniti kasno dekodiranje i dodati ponovno escapiranje. Isti princip teče i obrnutim smjerom kad se PDF sadržaj izvozi kao strukturirani tekst, kao u PDF-u u Markdown i DOCX semantičkom izvozu iz Delphija, gdje svaki doslovni znak mora biti escapiran za ciljnu sintaksu točno jednom
Brza referentna kontrolna lista
- Nadogradite na PDFlibPas v3.539.47 ili kasniji ako renderirate HTML ili Markdown koji sadrži korisničke podatke
- Escapirajte tekstualni sadržaj s
&prvim, pa<i>; ne pretvarajte navodnike za PDFlibPas tekst - Escapirajte jednom, na jedinoj točki gdje podaci ulaze u HTML string
- Držite nepouzdane vrijednosti izvan
href,srcistyle, ili ih validirajte protiv allow-liste - Očekujte da će se u tekstu dekodirati samo
<,>,&i ; drugi entiteti ostaju doslovni - Prenosite
LeftOverTextnatrag uDrawHTMLTextBoxnepromijenjen i ograničite petlju stranica - Markdown tokene nastavka predajte samo
DrawMarkdownTextBoxu iliDrawMarkdownTextu - Nikad ne pretražujte UTF-16 bajtne međuspremnike za bajtnim uzorcima; radite na cijelim kodnim jedinicama
HTML i Markdown renderiranje, izvoz izvještaja iz skupa podataka i ostatak mehanizma rasporeda isporučuju se u nativnom Pascal izvornom kodu PDF Library for Delphi, za Delphi i Free Pascal. Pogledajte stranicu proizvoda PDFlibPas za izdanja, podršku platformama i probno preuzimanje