PDF Library for Delphi (PDFlibPas) versijos iki v3.539.47 galėjo dekoduoti apsaugotą tekstą dukart, kai HTML arba Markdown piešiamas į PDF. DrawHTMLText ir DrawHTMLTextBox išanalizuoja HTML, suvienodina jį atgal į HTML, tada analizuoja dar kartą, todėl tekstas, parašytas kaip <unsafe>, pas antrą analizę atkeldavo kaip tikra žymė. Nuo v3.539.47 kiekvienas entitetas dekoduojamas lygiai vieną kartą, o tekstas vėl apsaugomas visur, kur jis virsta atgal į HTML
Scenarijus, kuris tai išryškina, visiškai kasdieniškas. Pagalbos tarnyba eksportuoja kreipimuisi į PDF, ir kliento komentaras keliauja į HTML šabloną. Kūrėjas padarė teisingai ir komentarą apsaugojo, tad <b> virto <b>. Atvaizduoklio viduje ta apsauga buvo tylioje atšaukta: komentas išeidavo pusjuodis, nežinomas žymės pavadinimas tiesiog dingdavo iš puslapio, o apsaugota nuoroda virto spausčiamąja nuorodos anotacija. Jokios išimties, jokio įspėjimo – visiškai taisyklingas PDF, kuris pasako ką nors kita, nei duomenys
Kodėl apsaugotas tekstas PDF viduje tampa tikra žyme?
Apsaugotas tekstas virto žyme todėl, kad atvaizduoklis turi dvi analizės fazes, o tarp jų esantis suvienodinimo žingsnis jau dekoduotą tekstą rašydavo atgal į HTML be pakartotinės apsaugos. Kiekviena dekodavimo operacija, kurią atlikdavo pirmoji analizė, antrajai atrodavo kaip gyva sintaksė
Dvi fazės egzistuoja dėl geros priežasties. Pirmoji analizė sukuria žymių ir žodžių elementų sąrašą. NormalizeParsedHTML tada išspręndžia stilių lentelės kaskadą: ji sugretina taisykles iš <style> blokų su kiekviena žyme, sujungia jas su inline style atributais, rezultatą užrašo ant žymės ir visą elementų sąrašą vėl užrašo į HTML eilutę. Išdėstymo fazė analizuoja tą suvienodintą eilutę. Tai ta pati mašinerija, kuri varo flexbox, CSS grid ir išnašų išdėstymą PDFlibPas HTML atvaizdavime
Brokas slypėjo tame, kaip užrašomi žodžiai. Žymės buvo rašomos atgal iš jų pirminės šaltinio formos, o žodžiai – jau dekoduota forma. Žodis, kurį pirmoji analizė buvo dekodavusi iš <unsafe> į <unsafe>, atkeldavo į suvienodintą HTML kaip neapsaugoti smailieji skliaustai, o antroji analizė jį skaitydavo kaip elementą. Aplink tą pagrindinį broką sėdėjo trys mažesnės nuotėkės, rodančios ta pačia kryptimi:
&nebuvo palaikomų entitetų aibėje, tadR&Dspausdinta pažodžiui ir nebuvo kaip parašyti pažodinį entiteto užrašą, tokį kaip<, kaip tekstą- Piešimo etapas
pakeisdavo antrą kartą, jau pasibaigus analizei, todėl pažodinis entiteto užrašas vis tiek galėjo dingsti pačioje pabaigoje - Markdown kodo apsauga praleisdavo ampersandą, o duomenų rinkinio eksportuotojas apsaugodavo vien smailiuosius skliaustus, todėl entitetų užrašai kode ar langelių reikšmėse būdavo dekoduojami kaip žymė
| Įvedimas, pasiekiantis atvaizduoklį | Iki v3.539.47 | Nuo v3.539.47 |
|---|---|---|
<unsafe> | Suvokiamas kaip žymė, tekstas nepasiekia puslapio | <unsafe> piešiamas kaip tekstas |
<b>x</b> | x piešiamas pusjuodžiu | <b>x</b> piešiamas kaip tekstas |
R&D | R&D spausdinta pažodžiui | R&D |
&lt; | &lt; spausdinta pažodžiui | < |
Markdown kodo intervalas su | Tapo nelaužoma tarpine | piešiamas kaip tekstas |
Duomenų rinkinio langelio reikšmė < | < | < |
Kaip v3.539.47 HTML entitetų dekodavimą sutraukia į vieną praėjimą
PDFlibPas v3.539.47 entitetų dekodavimą sutraukia į vieną praėjimą trimis susietais pakeitimais: analizatorius & dekoduoja paskutinį, piešimo etapas daugiau nieko nedekoduoja, o kiekviena vieta, kuri dekoduotus žodžius vėl paverčia HTML, juos pirmiausia vėl apsaugo
Teksto turiniui palaikoma entitetų aibė dabar yra <, >, & ir . Viskas kita, įskaitant skaitines nuorodas, tokias kaip A, ir vardinius entitetus, tokius kaip ", lieka pažodiniu tekstu. Ta riba svarbi tai, kaip apsaugosite savąjį įvedimą, kaip ir parodyta žemiau
Tvarka dekoduotojo viduje – pirmasis taisymas. Jei & būtų dekoduojamas pirmiausia, įvedimas &lt; taptų <, o sekantis pakeitimas jį paverstų < – dvigubas dekodavimas, įvykstantis vieno praėjimo viduje. Todėl ANSI žodžių kelias <, > ir pakeičia pirmiausia, o & – paskutinį, tad jo pagamintas ampersandas daugiau niekada nebetiriamas. UTF-16 žodžių kelias yra vienas iš kairės į dešinę ėjimas dvejų baitų žingsniais, kuris kiekvieną atitikmenį perrašo vietoje ir pro jį peržengia – tai ta pati garantija, tik struktūrinė
Antras taisymas pašalina vėlyvąjį pakeitimą iš piešimo etapo. Dekodavimas priklauso analizatoriui ir niekam kitam, todėl žodis, pasiekęs eilučių skaidytoją, yra galutinis tekstas
Trečias taisymas – ribos taisyklė. NormalizeParsedHTML dabar kiekvieną dekoduotą žodį prieš prijungdamas prie suvienodinto HTML apsaugoja &, < ir >. Antroji analizė jį dekoduoja atgal iki visai to paties teksto, tad grynas viso srauto efektas – vienas dekodavimas. Tęstinė eilutė laikosi tos pačios taisyklės: žodžiai, kurie netelpa į langą, prieš prijungiant prie LeftOverText apsaugojami, o likusios dalys kopijuojamos iš suvienodinto HTML, kuris jau yra apsaugota forma. Ciklas, surenkantis tuos likusius žodžius, dabar ribojamas ir žodžių skaičiumi, kur senasis repeat ciklas galėjo peržengti paskutinį žodį
Kodėl UTF-16BE apsauga negali naudoti baitų lygio pakeitimo?
UTF-16BE apsauga negali naudoti baitų lygio pakeitimo, nes dviejų baitų ampersando raštas gali persidengti per du nesusijusius simbolius. Vienintelis teisingas darbo vienetas – visa 16 bitų kodo vienetas
Atvaizduoklis Unicode žodžius saugo kaip big-endian UTF-16, supakuotus į baitų eilutes, pirmiausia aukštasis baitas. Ampersandas yra 00 26. Imkime U+0100 (didžioji lotynų A su brūkšniu, baitai 01 00), o po jos U+2603 (sniego senis, baitai 26 03). Baitų seka yra 01 00 26 03, o antras ir trečias baitai skaitosi 00 26. Baitų paieška po #0'&' randa ampersandą, kurio nėra, įklijuoja & baitus į dviejų simbolių vidurį ir kiekvieną paskesnį simbolį pastumsia per baitą
Tai ne egzotiškas pakraštys. Bet koks simbolis, kurio žemasis baitas nulinis, gali parodyti pirmąją pusę; U+4E00, vienas dažniausių CJK ideogramų, tinka. Smailieji skliaustai turi tą patį paviršių: 00 3C ir 00 3E atsiranda, kai po tokio simbolio eina vienas iš U+3C00 iki U+3EFF iš CJK Extension A. Taisymas EscapeHTMLWord viduje baitus išpakoja į WideString, apsaugoja simbolis po simbolio ir rezultatą supakuoja iš naujo. Dekoduotojo pusė jau buvo saugi, nes raštus ji tiria tik ties lyginiais kodo vienetų ribomis
Ta pati taisyklė galioja jūsų pačių kodui. Jeigu kada laikote UTF-16 tekstą kaip TBytes, pavyzdžiui po TEncoding.BigEndianUnicode.GetBytes, neieškokite jame baitų raštų. Konvertuokite atgal į eilutę ir dirbkite su simboliais
Markdown kodo blokai ir duomenų rinkinio eksportai: pirmiausia apsaugokite ampersandą
Nuo v3.539.47 abu HTML gamintojai PDFlibPas viduje – Markdown konverteris ir duomenų rinkinio eksportuotojas – ampersandą apsaugoja prieš smailiuosius skliaustus, tad vienintelis dekodavimas atvaizduoklyje atkuria lygiai pirminį tekstą
MarkdownToHTML viduje inline kodo intervalai ir aptverti arba įtraukti kodo blokai dabar & paverčia &, < – <, o > – >; tarpai tampa , o tabuliatorius – keturiais jais, kad įtrauka išliktų. Paprasta Markdown proza apsaugoja tik smailiuosius skliaustus, tad žalia HTML prozoje negali suleisti žymių, o autorius vis dar gali tyčia parašyti &, ko ir tikisi Markdown autorių. DrawMarkdownText ir DrawMarkdownTextBox naudoja tą pačią konversiją, tad kodas PDF atsiranda lygiai taip, kaip surinkta:
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
// Apžiūrėkite HTML: kode '&' tampa '&', o '<' – '<'
Html := Lib.MarkdownToHTML(Md);
Lib.SetOrigin(1); // viršuje kairėje, Y auga žemyn
Lib.SetMeasurementUnits(0); // taškai
// Puslapis rodo kodą lygiai taip, kaip surinkta, su entitetų užrašais
Lib.DrawMarkdownText(50, 50, 495, Md);
Lib.SaveToFile('code-sample.pdf');
finally
Lib.Free;
end;
end;
Duomenų rinkinio eksportuotojas – instruktyviausias atvejis. Iki v3.539.47 jis apsaugodavo vien smailiuosius skliaustus, ir tyčia: atvaizduoklis & nedekoduodavo, tad ampersando apsauga kiekviename langelyje, kuriame jis būtų, atspausdintų &. Apeinamasis kelias senajam atvaizduokliui buvo teisingas, o apskritai – klaidingas, nes langelio reikšmė, kuri atsitiktinai turėdavo <, būdavo dekoduojama į <. Atvaizduokliui pataisius, eksportuotojas & apsaugoja pirmiausia, ir tokia reikšmė kaip R&D < & atsiduria PDF pažodžiui. Jeigu tokiu būdu statote ataskaitas, apžvalga kaip eksportuoti TDataSet į PDF ataskaitą Delphi aprašo likusią eksportuotojo dalį
Kodėl ampersandas turi eiti pirmiausia, verta vieną kartą išdėstyti aiškiai. Apsaugokite < pirmiausia ir gausite <; tada apsaugokite & ir tai taps &lt;, ką teisingas vienkartinis dekodavimas parodys kaip <, o ne kaip <. Nuosekli pakeitimų grandinė teisinga tik tada, kai pats escape simbolis sutvarkomas anksčiau už viską, kas jį sukelia
Kaip turėtumėte apsaugoti nepatikimą tekstą DrawHTMLTextBox?
PDFlibPas HTML atvaizdavimui nepatikimą teksto turinį apsaugokite pakeisdami &, tada <, paskui > – lygiai vieną kartą – ir nepatikimų duomenų į atributų reikšmes visai neįleiskite
uses
System.SysUtils, PDFlibrary;
// Apsaugo nepatikimą tekstą PDFlibPas HTML tekstui.
// '&' turi būti pakeičiamas pirmiausia, kitaip ampersandas
// jau pagaminto '<' viduje būtų apsaugotas antrą kartą
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;
v3.539.47 komentaras, toks kaip Try <a href="https://example.com">this</a> & <b>, puslapyje atsiranda simbolis po simbolio. Iki v3.539.47 tas pats apsaugotas įvedimas galėjo pagimdyti gyvą nuorodos anotaciją – būtent tai paverčia rodymo triktį saugumo problema: kreipimosi komentaras niekada neturėtų galėti įkalti spausčiamos nuorodos į dokumentą, kuriuo jūsų personalas pasitiki
Pastebėkite, ko funkcija neapsaugoja. Universalūs HTML apsaugotojai dar paverčia " į ", o ' – į ', kas naršyklei teisinga. PDFlibPas teksto dekodavimas atpažįsta tik keturis anksčiau išvardytus entitetus, tad šie du atspausdintų pažodžiui kaip " ir '. Kabutės teksto turinyje nekenksmingos; jos svarbios tik atributų reikšmėse, o atvaizduoklis entitetų atributuose iš viso nedekoduoja. Saugus dizainas todėl – ne geresnis apsaugotojas, o taisyklė: nepatikimi duomenys niekada nekeliauja į href, src ar style. Jeigu nuorodos tikslas tikrai privalo ateiti iš naudotojo duomenų, patys patikrinkite jį pagal schemų ir simbolių leistinąjį sąrašą ir atmeskite viską, kas turi kabučių ar smailiųjų skliaustų
Iš taisymo tiesiogiai išplaukia dvi atnaujinimo pastabos:
- Jeigu jūsų kodas nustojo apsaugoti
&, nes senesnės versijos spausdino&pažodžiui, grąžinkite tai atgal. Be to, naudotojo tekstas, kuriame yra<, dabar rodomas kaip<– vis dar nekenksmingas tekstas, bet nebe tai, ką naudotojas surinko - Neapsaugokite dukart. Tekstas, praėjęs pro du apsaugotojus,
<atvaizduoja kaip matomą užrašą<, tad suraskite vienintelę ribą, kurioje jūsų duomenys įžengia į HTML, ir apsaugokite tik ten
Skirstymas į puslapius su LeftOverText nesugadinus apsaugų
DrawHTMLTextBox grąžina HTML, kuris netelpo, paprastai vadinamą LeftOverText, ir nuo v3.539.47 ta liekana išlaiko pažodinius entitetų užrašus ir apsaugotus smailiuosius skliaustus, kai perduodate ją kitam langui. Taisyklė kviečiantiesiems paprasta: grąžinkite ją nepakeistą
const
BoxLeft = 50;
BoxTop = 50;
BoxWidth = 495; // dydis A4 puslapiui taškais
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 jau apsaugotas variklio HTML: niekada jo neapsaugokite ar neiškoduokite
Rest := Lib.DrawHTMLTextBox(BoxLeft, BoxTop, BoxWidth, BoxHeight, Rest);
end;
if Rest <> '' then
raise Exception.CreateFmt('Content still left after %d pages', [MaxPages]);
end;
Į liekaną žiūrėkite kaip į nepermatomą daiktą. Tai suvienodintas variklio HTML, jau su išspręstais stiliais, tad neleiskite jos pro savąjį apsaugotoją, nedekoduokite ir nesujunkite su naudotojo tekstu. Puslapių riba – pigus draudimas: jei kuris elementas niekada netelpa į langą, neribotas ciklas neturi natūralaus išėjimo
Markdown turi savą tęstinumą. DrawMarkdownTextBox grąžina tokeną, prasidedantį vidiniu žymekliu, kad kitas kvietimas galėtų praleisti konversiją; grąžinkite jį DrawMarkdownTextBox arba DrawMarkdownText, o ne į HTML įėjimo taškus, kurie žymeklį nupieštų kaip tekstą
Bendra pamoka: dekoduoti vieną kartą, kiekvienoje riboje koduoti iš naujo
Bet koks srautas, kuris analizuoja tekstą, rezultatą užrašo atgal į tą pačią sintaksę ir analizuoja dar kartą, turi dekodavimą traktuoti kaip operaciją, vykstančią lygiai vienoje vietoje, ir turi koduoti iš naujo kiekvienoje riboje, kurioje dekoduotas tekstas vėl tampa sintakse. Šablonų varikliai, HTML sanitarai ir Markdown–HTML–PDF grandinės turi tą patį pavidalą ir klysta taip pat, kai serializatorius užmiršta, kad gamina žymę
Simptomai numanomi, kai tik žinai pavidalą. Per mažai perkodavimo – duomenys virsta sintakse, tai injekcijos kryptis. Per daug kodavimo arba dukart paleistas dekoduotojas – skaitytojas mato entitetų užrašus arba jie pradingsta, tai rodymo kryptis. Vienos krypties taisymas paprastai sulaužo kitą, todėl PDFlibPas taisymui teko viename leidime pridėti & dekodavimą, perrūšiuoti jį, pašalinti vėlyvąjį dekodavimą ir pridėti pakartotinę apsaugą. Tas pats principas veikia priešinga kryptimi, kai PDF turinys eksportuojamas kaip struktūrinis tekstas, kaip PDF į Markdown ir DOCX semantiniame eksporte iš Delphi, kur kiekvienas pažodinis simbolis turi būti apsaugotas tikslinės sintaksės lygiai vieną kartą
Trumpa atmintinė
- Atnaujinkite iki PDFlibPas v3.539.47 ar vėlesnės, jeigu atvaizduojate HTML arba Markdown su naudotojo duomenimis
- Teksto turinį apsaugokite iš pradžių
&, paskui<ir>; PDFlibPas tekstui kabučių nekonvertuokite - Apsaugokite vieną kartą, vienintelėje vietoje, kur duomenys įžengia į HTML eilutę
- Nepatikimų reikšmių neleiskite į
href,srcirstylearba patikrinkite jas pagal leistinąjį sąrašą - Tekste tikėkitės dekoduojamų vien
<,>,&ir ; kiti entitetai lieka pažodiniai LeftOverTextgrąžinkiteDrawHTMLTextBoxnepakeistą ir ribokite puslapių ciklą- Markdown tęstinumo tokenus perduokite tik
DrawMarkdownTextBoxarbaDrawMarkdownText - Niekada neieškokite UTF-16 baitų buferiuose baitų raštų; dirbkite tikrais kodo vienetais
HTML ir Markdown atvaizdavimas, duomenų rinkinio ataskaitų eksportas ir likusioji išdėstymo variklio dalis keliauja natyvioje PDF Library for Delphi Pascal šaltininko pakuotėje, skirtoje Delphi ir Free Pascal. Leidimams, platformų palaikymui ir bandomajam parsisiuntimui žiūrėkite PDFlibPas produkto puslapį