2011. október 30., vasárnap

OWASP AntiSamy Javaban

A webes biztonság kérdése egyidős magával az internettel, hiszen a hálózaton elérhető adatokat felhasználók milliói tekintik meg napról-napra. Mivel a weboldalak végeredményben a kliensek gépein futnak, hamar beláthatjuk, hogy a biztonsági kérdések nem elhanyagolhatóak, és mind a klienseknek, mind a weboldalak üzemeltetőinek fontos, hogy a kiszolgált tartalom hiteles és biztonságos legyen. Tipikus támadási forma az un. XSS, amikor pl. egy az oldalba ágyazott külső Javascript próbál szenzitív információkhoz hozzájutni a böngésző valamely biztonsági hibáját kihasználva. Az ilyen és ehhez hasonló problémák megoldására jött létre 2001-ben OWASP Antisamy Project, melynek célja nyílt forrású alternatívát, és szabványt kínálni az alkalmazások megvédésére.

Mint minden 3rd party fejlesztés, ez is úgy kezdődik, hogy letöltjük a programkönyvtár lehetőleg legfrissebb verzióját. AntiSamy jelenleg az 1.4.4-es verziónál tart. A dokumentációval ellentétbe a csomag függőségeit nem lehet letölteni, rákeresve a antisamy-required-libs.zip kulcsszavakra találtam egy oldalt. A készítők szerint deprecated ami a zip-be van, de a próba erejéig megteszi, éles használatra össze kell vadászni a függőségeket. A szoftver konfigurációja eléggé bonyolult, hosszas dokumentáció olvasással és tanulással egész biztos össze lehet dobni egy normális konfig XML-t, de szerencsére több nagyobb felhasználó is rendelkezésre bocsátotta saját összeállítását, így nyugodtan mazsolázgathatunk az eBay, MySpace, vagy akár Slashdot beállításaiból.

Az általam készített tesztprogram az alábbi (belecsempészve kis Java 7-et):

import java.io.IOException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.owasp.validator.html.*;


public class AntiSamyTest {

    public static void main(String[] args) {
        try {
            URL dirtyHtmlUrl = AntiSamyTest.class.getResource("dirtyHtml.html");
            Path dirtyHtmlPath = FileSystems.getDefault().getPath(dirtyHtmlUrl.getFile());
            List<String> lines = Files.readAllLines(dirtyHtmlPath, Charset.defaultCharset());
   
            URL configXmlUrl = AntiSamyTest.class.getResource("antisamy-ebay-1.4.4.xml");
            Policy policy = Policy.getInstance(configXmlUrl.getFile());
            
            AntiSamy as = new AntiSamy();
            CleanResults cleanResult = as.scan(concatString(lines), policy);

            System.out.println(cleanResult.getCleanHTML());
        } catch (IOException | PolicyException | ScanException ex) {
            throw new RuntimeException(ex);
        }
    }
 
    private static String concatString(List<String> input) {
        StringBuilder output = new StringBuilder();
        for(String line : input) output.append(line);
        return output.toString();
    }
}
A dirtyHtml.html fájl tartalma szabvány HTML (HTML, HEAD, BODY). A programot futtatva láthatjuk, hogy a body tartalmán kívül minden HTML taget kidobott az AntiSamy. Azért mondom, hogy minden HTML taget, mert ha van pl. title a headbe, a tartalma bizony ottmarad, tehát csak a sallang kerül ki. Szerintem ez utóbbi működés konfigurálható (fixme).

Az elméleti ismerkedés után ideje valami komolyabb megbizatást adni Samykének. Személy szerint Liferay fejlesztő vagyok, így szinte evidens, hogy erre esett a választásom. A Liferay 6-os verziója óta létezik egy Sanitizers-nek nevezett funkcionalitás, amely, bár még nem teljeskörű, mégis segít az igényes programozónak, a kritikus user inputokat szűrni. A funkcionalitás mint írtam nem teljeskörű, ugyanis egyelőre csak a Blog bejegyzéseket tudjuk kontrollálni out-of-the-box. A dolgunk egyszerű, a portal.properties-ben van egy sanitizer.impl paraméter, amit a portal-ext.properties-ben felül tudunk definiálni. Az alapbeállítás a com.liferay.portal.sanitizer.DummySanitizerImpl osztályra mutat, ami jóformán semmit nem csinál, viszont jó kiindulási pont lehet saját Saniterünk elkészítéséhez. Létezik a Liferaynek beépített osztálya com.liferay.portal.kernel.sanitizer.SanitizerUtil képében, választhatjuk ezt is, de saját megoldást is minden további nélkül.

Miután Blog bejegyzéseinket megvédtük a sokszor óvatlan bloggerektől, sajnos nem dőlhetünk hátra nyugodtan, mivel a Liferayben is, mint minden CMS-ben, nem csak Blogot szerkesztenek a felhasználók, hanem számtalan egyéb módon is lehetőségük van HTML szöveget a rendszerbe juttatni. Mivel "gyári" támogatás még nincs ezen bejegyzésekre, nincs más lehetőségünk, mint hook-ot írni. A Plugin tárolóban van egy "antisamy hook", amely alapján könnyedén megírhatjuk saját kiterjesztésünket. A legjobb módszer un. Model Wrapper Hook készítése. Hozzunk létre egy hook-ot, majd a liferay-hook.xml fájlba írjuk be a felülírandó szervíz definícióját

<hook>
    <service>
        <service-type>com.liferay.portlet.wiki.service.WikiPageLocalService</service-type>
        <service-impl>com.test.hooks.SaniterWikiPageLocalService</service-impl>
    </service>
</hook>
Majd írjuk meg saját osztályjunkat
import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.kernel.exception.SystemException;
import com.liferay.portal.kernel.sanitizer.SanitizerUtil;
import com.liferay.portal.kernel.util.ContentTypes;
import com.liferay.portal.service.ServiceContext;
import com.liferay.portlet.wiki.model.WikiPage;
import com.liferay.portlet.wiki.service.WikiPageLocalService;
import com.liferay.portlet.wiki.service.WikiPageLocalServiceWrapper;

public class SaniterWikiPageLocalService extends WikiPageLocalServiceWrapper {

    public SaniterWikiPageLocalService(WikiPageLocalService wikiPageLocalService) {
        super(wikiPageLocalService);
    }
 
    public WikiPage addPage(
            long userId, long nodeId, String title, double version,
            String content, String summary, boolean minorEdit, String format,
            boolean head, String parentTitle, String redirectTitle,
            ServiceContext serviceContext)
            throws PortalException, SystemException {

        String sanitizedContent = SanitizerUtil.sanitize(
            serviceContext.getCompanyId(), serviceContext.getScopeGroupId(),
            userId, WikiPage.class.getName(), 0, ContentTypes.TEXT_HTML, content);

        return super.addPage(userId, nodeId, title, version,
            sanitizedContent, summary, minorEdit, format,
            head, parentTitle, redirectTitle,
            serviceContext);
    }
}
Bár én a Wiki-t választottam példának, ez alapján bármely más szervízre megírható a szűrés.

Nincsenek megjegyzések:

Megjegyzés küldése