Flow — Eine Datenfluss-Beschreibungssprache für objektorientierte Programme Autoren: Marek Jawurek Stefan Mandel IESE-Report Nr. 086.08/D Version 1.0 November 2008 Eine Publikation des Fraunhofer IESE Das Fraunhofer IESE ist ein Institut der Fraunhofer-Gesellschaft. Das Institut transferiert innovative SoftwareEntwicklungstechniken, -Methoden und -Werkzeuge in die industrielle Praxis. Es hilft Unternehmen, bedarfsgerechte Software-Kompetenzen aufzubauen und eine wettbewerbsfähige Marktposition zu erlangen. Das Fraunhofer IESE steht unter der Leitung von Prof. Dr. Dieter Rombach (geschäftsführend) Prof. Dr. Peter Liggesmeyer Fraunhofer-Platz 1 67663 Kaiserslautern Kurzfassung Der Bericht beschreibt Flow, eine Sprache zur Spezifikation von Datenflüssen in objektorientierten Programmen. Flow wurde im Rahmen des Projekts SecFlow entwickelt, dessen Zielsetzung es ist, sicherheitskritische Datenflüsse in Quellcode mittels werkzeuggestützter Analyse zu ermitteln. In SecFlow werden Flow-Spezifikationen zum einen genutzt, um sicherheitsrelevante Datenquellen und Datensenken zu bezeichnen. Zum anderen dienen sie dazu, das Datenflussverhalten von Software-Komponenten zu charakterisieren, deren Inneres jenseits des Analysehorizonts liegt oder deren Quellcode für Analysen nicht zur Verfügung steht: Eine geeignete FlowCharakterisierung ermöglicht es, Datenflüsse auch durch opaque SoftwareKomponenten hindurch weiterzuverfolgen. Das Verbundprojekt »SecFlow: Automatische Erkennung sicherheitskritischer Datenflüsse in Quellcode« wird vom Bundesministerium für Bildung und Forschung (BMBF) unter dem Förderkennzeichen 01ISF09C gefördert. Schlagwörter: Datenfluss, Quellcode-Analyse, Java, C#, IT-Sicherheit, SecFlow, BMBF Keywords: data flow, source code analysis, program analysis, computer software – evaluation, computer software – quality control, Java programming language, C# programming language, information security, IT security, SecFlow, BMBF Copyright © Fraunhofer IESE 2008 v Inhaltsverzeichnis Kurzfassung Copyright © Fraunhofer IESE 2008 v Inhaltsverzeichnis vii Verzeichnis der Flow-Beispiele ix Tabellenverzeichnis xi 1 Datenflüsse und Datenfluss-Spezifikationen 1 1.1 Datenflüsse 1 1.2 Datenflussanalysen 1 1.3 Datenfluss-Spezifikationen 2 1.4 Die Datenfluss-Beschreibungssprache Flow 2 2 Spezifizieren in Flow 3 3 Grundstruktur einer Flow Summary 5 3.1 Language Hint 5 3.2 Imports 5 3.3 Summary-Definitionen 6 3.4 Attribute 6 3.5 (Statische) Initialisierer 7 3.6 Konstruktoren und Methoden 9 4 Flow-Anweisungen 4.1 Eingangs- und Ausgangsbezeichner einer Flow-Anweisung 11 4.2 Binden von Quellen und Senken 12 4.3 Flows 13 4.4 Funktionale Ausdrücke 14 11 vii viii 4.5 Pipes 16 4.6 Objekt-Instanziierung 16 4.7 Daten-Generierung/Konsumierung 17 5 Fazit 19 Anhang — Anwendungsbeispiele 21 Abkürzungsverzeichnis 25 Quellenverzeichnis 27 Copyright © Fraunhofer IESE 2008 Verzeichnis der Flow-Beispiele Beispiel 1 Verwendung von Imports .....................................................6 Beispiel 2 Summary-Definition..............................................................6 Beispiel 3 Verwendung von Attributen .................................................7 Beispiel 4 Flow-Initialisierer...................................................................8 Beispiel 5 Datenfluss-Spezifikation für Methoden und Konstruktoren ...9 Beispiel 6 Flow-Bezeichner .................................................................11 Beispiel 7 Binden von Quellen und Senken .........................................12 Beispiel 8 Propagieren gebundener Quellen und Senken.....................12 Beispiel 9 Spezifizieren von Flüssen.....................................................14 Beispiel 10 Verwendung von Pipes .......................................................16 Beispiel 11 Objektinstanziierung...........................................................16 Beispiel 12 Spezifizieren eines Datengenerators ....................................17 Beispiel 13 Spezifizieren eines Datenkonsumenten ...............................17 Beispiel 14 Flow-Spezifikation der Klasse java.lang.String......................21 Beispiel 15 Flow-Spezifikation der Klasse.java.net.URLEncoder..............22 Beispiel 16 Flow-Spezifikation der Klasse.java.net.URLDecoder .............23 Copyright © Fraunhofer IESE 2008 ix Tabellenverzeichnis Tabelle 1 Copyright © Fraunhofer IESE 2008 Vordefinierte Flow-Funktionen............................................14 xi Datenflüsse und Datenfluss-Spezifikationen 1 Datenflüsse und Datenfluss-Spezifikationen Das Datenflussverhalten eines Programms ist ein Software-Merkmal, das wesentlich dazu beiträgt, die Bedeutung der Programmlogik zu erschließen. Die Charakterisierung von Datenflüssen ist daher ein wichtiger Schritt bei der Programmanalyse. 1.1 Datenflüsse Computerprogramme lesen Eingabedaten und transformieren diese Schritt für Schritt in die gesuchten Ausgabedaten. Das gilt nicht nur für Anwendungen im Bereich der konventionellen Informationsverarbeitung, sondern auch für eingebettete, reaktive und echtzeitorientierte Software, die Sensor-Eingaben entgegennimmt, um daraus geeignete Steuerimpulse für eine Aktorik abzuleiten. Bei jedem Verarbeitungsschritt werden die vorliegenden Daten weitergereicht, von Anweisung zu Anweisung, Funktion zu Funktion und Modul zu Modul. Die Eingabedaten fließen also von den möglichen Eingabeschnittstellen eines Programms über verzweigte, oft schwer zu überblickende Wege bis hin zu den Ausgabeschnittstellen. Einen solchen potentiellen Weg durch das Programm nennen wir einen Datenfluss. 1.2 Datenflussanalysen Auf ihrem Weg durch das Programm unterliegen die Daten vielfältigen Veränderungen. Sie werden umformatiert, zerlegt und neu kombiniert. Dies erschwert es, den genauen Datenfluss eines konkreten Datenobjekts zu verfolgen. Datenflüsse neigen zur Verästelung: Einige Zweige erweisen sich als Sackgassen, da die entsprechenden Datenfragmente gelöscht werden, während auf anderen Datenfluss-Zweigen Bruchteile der Ausgangsdaten weiter propagiert werden und so Einfluss auf den weiteren Verlauf der Berechnung nehmen. Um die Wirkungsweise eines Programms zu verstehen ist es wichtig, alle Datenflüsse des Programms zu überblicken. Ein tiefes Datenflussverständnis ist zum Beispiel erforderlich, um aus Quellprogrammen möglichst effizienten, laufzeit- oder speicheroptimierten Maschinencode zu erzeugen; Datenflussanalysen sind deshalb ein fester Bestandteil moderner Übersetzerprogramme. Datenflussbetrachtungen sind aber auch hilfreich, um beim Testen von Software möglichst aussagekräftige Testfälle auszuwählen und sicherzustellen, dass alle Teile eines Softwaresystems ausreichend getestet werden. Copyright © Fraunhofer IESE 2008 1 Datenflüsse und Datenfluss-Spezifikationen 1.3 Datenfluss-Spezifikationen Im Idealfall ermöglicht eine korrekte und vollständige Datenflussanalyse sogar ein formales Verifizieren der Korrektheit eines Programms — dem sind aufgrund der Komplexität heutiger Software jedoch enge Grenzen gesetzt. In der Praxis ist es oft nicht möglich, Datenflüsse vollautomatisch zu ermitteln. Eine vollständige Analyse scheitert oft an zu hohem Rechenaufwand oder einfach daran, dass der innere Aufbau einer Software-Komponente nicht bekannt ist, sondern nur eine Beschreibung des Schnittstellenverhaltens zur Verfügung steht. Daher ist es oft erforderlich, Teile eines Datenflusses manuell zu spezifizieren; die Spezifikation kann in automatische Datenfluss-Analysewerkzeuge eingespeist werden, um »blinde Flecken« des Werkzeugs zu überbrücken und semantisches Wissen beizusteuern, das — aus praktischer Erwägung oder aufgrund fundamentaler Beschränkungen der Logik — nicht automatisch berechenbar ist. 1.4 Die Datenfluss-Beschreibungssprache Flow Der vorliegende Bericht beschreibt Flow, einer Sprache zur Spezifikation von Datenflüssen in objektorientierten Programmen. Flow wurde im Rahmen des Projekts SecFlow [Peine et al. 2006, Mandel&Peine 2007] entwickelt, dessen Zielsetzung es ist, sicherheitskritische Datenflüsse in Quellcode mittels werkzeuggestützter Analyse zu ermitteln. In SecFlow werden Flow-Spezifikationen zum einen genutzt, um sicherheitsrelevante Datenquellen und Datensenken zu bezeichnen. Zum anderen dienen sie dazu, das Datenflussverhalten von Software-Komponenten zu charakterisieren, deren Inneres jenseits des Analysehorizonts liegt oder deren Quellcode für Analysen nicht zur Verfügung steht: Eine geeignete FlowCharakterisierung ermöglicht es, Datenflüsse auch durch opaque SoftwareKomponenten hindurch weiterzuverfolgen. Unsere SecFlow-Analyseumgebung verfügt über Werkzeuge zur Erstellung, Prüfung und Übersetzung von FlowSpezifikationen [Wagner 2008]. Die folgende Beschreibung gliedert sich wie folgt: Kapitel 2 skizziert den Spezifikationsansatz der Sprache Flow. Kapitel 3 beschreibt die grundlegende Struktur von Flow-Spezifikationen und die dafür verfügbaren Sprachelemente. Kapitel 4 stellt die verschiedenen Anweisungstypen vor, die Flow zur Charakterisierung eines Datenflussverhaltens bietet. Kurze Flow-Beispiele illustrieren in den Kapiteln 3 und 4 die Verwendung der Flow-Sprachmittel. 2 Copyright © Fraunhofer IESE 2008 Spezifizieren in Flow 2 Spezifizieren in Flow Die Sprache ››Flow‹‹ wurde konzipiert, um bestimmte Teile eines objektorientierten Programms in ihrem Datenflussverhalten zu beschreiben. Dies können Teile sein, welche implizit benötigt werden (Bibliotheken, vordefinierte Datentypen), aber nicht im Quelltext zu Verfügung stehen. Es können aber auch Programmteile außerhalb des Analysehorizonts sein, deren Quelltext zwar verfügbar ist, die aber nur auf ihr Datenflussverhalten reduziert werden sollen: Die Analyse der Flow-Beschreibung einer Bibliothek ist deutlich schneller als die Analyse der entsprechenden Bibliothek. Eine Datenfluss-Spezifikation ist außerdem für vordefinierte Sprachkonstrukte/Typen der Analysesprache unumgänglich, um die Semantik nachzubilden, die ihnen durch den Compiler (bzw. den Interpreter) zugeschrieben wird. Den Aspekt des Datenflusses einer Klasse beschreiben wir durch eine sogenannte Flow Summary — eine Zusammenfassung des Datenflussverhaltens auf abstraktem Niveau. Die Flow Summary beschränkt sich ausschließlich auf die Datenfluss-Eigenschaften der Klasse; sie spezifiziert nicht deren gesamtes Verhalten. Im Folgenden beschreiben wir die Struktur einer Flow Summary, die verschiedenen Flow-Sprachelemente und die Anwendung von Flow zur Modellierung von Datenflüssen. Anhand einiger beispielhafter Klassen illustrieren wir die Verwendung der Flow-Sprachelemente. Zur Beschreibung der Sprache Flow verwenden wir folgende Konventionen: Metasymbole der Flow-Grammatik werden im Schrifttyp »METASYMBOL« gesetzt. Platzhalter werden immer mit <> eingeschlossen und im Schrifttyp »<Platzhalter>« gesetzt. Sie sind in der Spezifikation durch sinngemäße Werte zu ersetzen. Terminalsymbole, zum Beispiel Flow-Schlüsselwörter oder Operatorzeichen, werden im Schrifttyp »Flowtext« gesetzt. Sie sind buchstabengetreu in die Flow-Spezifikation zu übernehmen. Einzelne Flow-Anweisungen und die verschiedenen Abschnitte einer Flow Summary werden durch eine oder mehr neue Zeilen voneinander getrennt. Copyright © Fraunhofer IESE 2008 3 Grundstruktur einer Flow Summary 3 Grundstruktur einer Flow Summary Die Grundstruktur einer Flow Summary sieht wie folgt aus: FLOWFILE ::= 3.1 LANGUAGEHINT IMPORTS SUMMARYDEFINITIONS Language Hint Der Language Hint zeigt an, für welche Analysesprache eine Flow Summary geschrieben wurde. Obwohl die Flow Summary selber nicht sprachabhängig ist, werden je nach Analysesprache unterschiedliche Name Resolver für die Normalisierung der Typnamen verwendet. Daher ist dieser Hinweis verpflichtend. Er sieht folgendermaßen aus: LANGUAGEHINT ::= 3.2 @'Java' | @'.NET' Imports Mit Hilfe der Imports werden zwei Ziele erreicht: Erstens werden alle in der entsprechenden Flow Summary verwendeten Klassen importiert. Dies ist für alle in der Summary verwendeten Klassen nötig! Außerdem können Klassennamen mit Hilfe von Umschreibungsregeln Kurzbezeichner zugeordnet werden, welche im weiteren Verlauf der Summary an Stelle des vollen Klassennamens verwendet werden. Die Import-Klausel hat folgendes Format: IMPORTS IMPORT ::= ::= IMPORT [IMPORTS] <local classname> [ ='<original classname>'] @ [<package> ] Dies bewirkt einen Import der Klasse <original classname> aus dem Package <package> und definiert für den weiteren Verlauf der Flow Summary den lokalen Bezeichner <local classname> für diese Klasse. <original classname> und <package> sind die exakten Bezeichnungen des Klassennamens bzw. des Package-Namens in der Analysesprache. <local classname> darf nur Buchstaben und Ziffern enthalten. Wenn die Umschreibung (mittels ='<original classname>' ) weggelassen wird, muss <local classname> dem tatsächlichen Klassennamen in <package> entsprechen. Importe und Umbenennungen sind nur für Klassen vorgeschrieben und möglich. Copyright © Fraunhofer IESE 2008 5 Grundstruktur einer Flow Summary Beispiel 1 Verwendung von Imports @'Java' // benennt java.lang.String lokal in str um str='String' @ [java.lang] // benennt java.lang.Object lokal in obj um obj=’Object’ @ [java.lang] 3.3 Summary-Definitionen Die Syntax der eigentlichen Summaries hat folgende Form: SUMMARYDEFINITIONS ::= SUMMARY ::= SUMMARY [SUMMARYDEFINITIONS] summary <name> [ : SUPERTYPES] { [ATTRIBUTES] [INITIALIZERS] [METHODS] } Eine Summary beschreibt eine Klasse bzw. Übersetzungseinheit. Der Name der korrespondierenden Klasse ist <name>. SUPERTYPES ::= SUPERTYPE ::= SUPERTYPE [, SUPERTYPES ] <parent class> | <flow name> ist eine kommagetrennte Liste mit Bezeichnern. Die Summary erbt von allen Summaries, welche durch diese Bezeichner repräsentiert werden. Typen oder Interfaces der Analysesprache sind als Bezeichner auch erlaubt, allerdings haben diese keinen Effekt: Die Möglichkeit, in Flow auch Typ- oder Interface-Angaben zu berücksichtigen, wurde für zukünftige Erweiterungen offen gehalten. SUPERTYPES Beispiel 2 Summary-Definition … summary MyString : String { … } 3.4 Attribute Innerhalb einer Summary können beliebig viele Attribute definiert werden. ATTRIBUTES ::= ATTRIBUTE [ATTRIBUTES] Attribute sind lediglich Behelfskonstrukte, um Datenflüsse durch die von der Summary beschriebenen Datentypen hindurch darstellen zu können. Attribute haben selbst keinen Typ. Sie werden wie Objektvariablen mit unbeschränktem 6 Copyright © Fraunhofer IESE 2008 Grundstruktur einer Flow Summary Zugriff behandelt. Die Definition eines Attributs im Rumpf einer Klasse definiert gleichzeitig aber auch ein Klassenattribut. Unterschieden wird zwischen diesen beiden nur durch die Art des Zugriffs (Typ.Attribut für Klassenattribute, Ausdruck.Attribut für Attribute). Eine Attribut-Deklaration hat daher die folgende Form: ATTRIBUTE ::= <attribute>; | <attribute> { [STATEMENTS] } Attributnamen dürfen nur aus Buchstaben und Ziffern bestehen, wobei der Name mit einem Buchstaben beginnen muss. Eine spezielle Form des Attributes kann Flow-Statements enthalten. Diese FlowStatements werden bei jedem Zugriff auf das Attribut ausgewertet. Sie entsprechen also einer Funktion ohne Argumente. Dieser Mechanismus wird zum Beispiel für das Attribut tostring verwendet, das man zur Modellierung des folgenden Sachverhalts benötigt :Die toStringMethode einer java.lang.List ruft rekursiv die toString-Methoden aller in der Liste enthaltenen Objekte auf und baut deren Rückgabewerte in den eigenen Rückgabewert ein. Es ist aber nicht möglich, in einer durch Flow beschriebenen Methode Java-Methoden aufzurufen, so wie es in diesem Falle nötig wäre. Um den Sachverhalt trotzdem darstellen zu können, wurde das Attribut tostring mit zusätzlichen Flow-Statements eingeführt. Die toStringMethoden aller Collections greifen auf das tostring-Attribut der in ihnen enthaltenen Objekte zu und führen dadurch zur Auswertung entsprechender, im Attribut definierter, Flow-Statements. Diese Konstruktion ist auch für andere, ähnliche Beziehungen zwischen in Flow spezifizierten Klassen denkbar. Beispiel 3 Verwendung von Attributen … summary MyString : String { length; tostring { <- this.length } } 3.5 (Statische) Initialisierer In den Initialisierern werden — ähnlich einem Konstruktur in konventionellen Programmiersprachen — Vorbereitungen für das Objekt getroffen. Ein Initialisierer in Flow unterscheidet sich allerdings unter anderem von Konstruktoren anderer Sprachen dadurch, dass er keine Parameter akzeptiert. Er kann zusätzlich zu den Konstruktoren der Analysesprache existieren, die auch in Copyright © Fraunhofer IESE 2008 7 Grundstruktur einer Flow Summary der Summary modelliert werden können. Allerdings werden beim Instanziieren einer Klasse, die durch eine Flow Summary beschrieben wird, zuerst die FlowInitialisierer interpretiert. Die Interpretation des Konstruktors der Analysesprache findet erst danach statt, sobald das Objekt durch Flow fertig vorbereitet wurde. Die Syntax der Initialisierer ist die folgende: INITIALIZERS INITIALIZER ::= ::= INITIALIZER [INITIALIZERS] [static] { [STATEMENTS] } Statische Initialisierer (die noch vor der Initialisierung von Objekten — und dann genau einmal — interpretiert werden) verwenden das Flow-Schlüsselwort static. Sie werden ausgewertet, sobald die entsprechende Klasse im Analyseumfang referenziert wurde. Beispiel 4 Flow-Initialisierer … summary MyString : String { length; { this.length <- [Integer] } } Die Reihenfolge der Umwandlung der Initialisierer und Konstruktoren bei der ersten Referenzierung einer Klasse im Analyseumfang wie in Beispielzeile Collection c = (Collection) new Hashtable<Integer,String>(); ist folgende: 1. Wenn bei der Analyse des Call Graphs festgestellt wird, dass diese Zeile im Programm erreichbar ist, wird der statische Initialisierer der Klasse Hashtable vor dem Beginn der Analyse des Programms angewendet, sofern er existiert. 2. Sobald ein neues Objekt der entsprechenden Klasse (in diesem Fall Hashtable) instanziiert wird, wird der (nicht-statische) Initialisierer der Klasse angewendet, sofern er existiert. 3. Als letztes wird der Konstruktor der Klasse, welcher bei der Instanziierung verwendet wird, angewendet. Initialisierer werden auch angewendet, wenn Anweisungen innerhalb einer Flow Summary neue Flow-Objekte instanziieren bzw. Flow-Klassen referenzieren. Allerdings wird in diesem Fall kein Konstruktor der Analysesprache angewendet. 8 Copyright © Fraunhofer IESE 2008 Grundstruktur einer Flow Summary 3.6 Konstruktoren und Methoden Um die Datenflüsse einer Klasse vollständig und möglichst genau modellieren zu können, muss auch spezifiziert werden, welche Datenflüsse beim Aufruf von Methoden bzw. Konstruktoren stattfinden. Dazu wird die Methode bzw. der Konstruktor der Analysesprache in Flow mit einer mit der Java-Syntax identischen Syntax und Semantik notiert, wobei Konstruktoren durch Methoden ohne Rückgabewert dargestellt werden. METHODS METHOD PARAMETERLIST Beispiel 5 ::= METHOD [METHODS] ::= [<return type> ] <method name> ( [PARAMETERLIST] ) { [STATEMENTS] } ::= <parameter type> <parameter name> [, PARAMETERLIST] Datenfluss-Spezifikation für Methoden und Konstruktoren … summary MyString : String { … String(char[] chars) { … } String append(String tail){ … } } Copyright © Fraunhofer IESE 2008 9 Flow-Anweisungen 4 Flow-Anweisungen Flow-Anweisungen (in den obigen Beispielen als Statements bezeichnet) drücken die Erzeugung von Daten, die eigentlichen Datenflüsse und das Abfließen von Daten in Senken aus. Sie können nur in Initialisierern und Methoden eingesetzt werden. STATEMENTS ::= STATEMENT ::= STATEMENT [STATEMENTS] BINDING | FLOW | RETURNFLOW | PIPE | OBJECTGENERATOR | DATAGENERATOR | DATACONSUMPTION Die verschiedenen Arten von Statements werden in den folgenden Kapiteln noch weiter erläutert. 4.1 Eingangs- und Ausgangsbezeichner einer Flow-Anweisung Als Ziel- und Startpunkt von Datenflüssen werden Bezeichner benötigt. Ein Bezeichner besteht aus einem oder mehreren verschiedenen Elementen, welche durch Punkte voneinander getrennt sind. Ein solch zusammengesetzter Bezeichner wird hier auch als Pfad bezeichnet. Elemente eines Pfades können Klassenattribute, Attribute oder lokale Variablen sein (Parameter werden wie lokale Variablen betrachtet). Die Syntax eines Pfads ist: PATH PATHREST ::= ::= {<local variable> | <type> | this } [PATHREST] {<attribute> | []} [PATHREST] Auf Klassenattribute wird über die Angabe des Klassennamens im Bezeichnerpfad zugegriffen, auf Objektvariablen über den Namen des Objekts bzw. das this Schlüsselwort im Bezeichnerpfad. Ein [] steht für den Zugriff auf ein Array. Beispiel 6 Flow-Bezeichner … this.file shop.items[].length String.stringcount … // Instanzattribut // Instanzattribut // Klassenattribut Zusätzlich zu Klassen-Attributen können in Flow auch lokale Variablen definiert werden und auch in Pfaden eingesetzt werden. Sie werden implizit bei der ersten Verwendung definiert und überschrieben, wie auch z.B. in Java, Copyright © Fraunhofer IESE 2008 11 Flow-Anweisungen gleichnamige Attribute der Klasse nur in ihrem lokalen Kontext (mittels this.attribute kann auch in diesem lokalen Kontext explizit auf das Instanzattribut zugegriffen werden). 4.2 Binden von Quellen und Senken Daten, die von außerhalb des Systems — also vom Benutzer oder anderen Systemen — in das Programm einfließen, betreten das Programm durch eine Quelle. Eine Quelle kann eine Datei, ein Netzwerkport, ein HTTP-Request oder ähnliches sein. Senken stellen Kontaktstellen des Programms mit seiner Außenwelt dar, durch welche Daten nach außen abfließen. Quellen und Senken müssen mit Hilfe von sogenannten Bindings mit Bezeichnern in Flow verbunden werden. Die so gebundenen Quellen und Senken werden in der FlowSpezifikation durch den Bezeichner repräsentiert. Der Binding-Operator hat folgende Syntax: BINDING ::= PATH ~ [source('<type>') | sink('<type>') | PATH ] <type> steht für die Art der Quelle bzw. Senke und wird von Flow und dem Analyse-Werkzeug nicht vorgegeben und nicht interpretiert. Bei der Ausgabe der Analyseergebnisse wird der <type> als Ursprung bzw. Ziel des Datenflusses angegeben, eine aussagekräftige Bezeichnung ist also sinnvoll. Beispiel 7 Binden von Quellen und Senken … summary File { filesystem; { this.filesystem ~ sink('FILESYSTEM') this.filesystem ~ source('FILESYSTEM') } } Quellen und Senken können zwischen verschiedenen Objekten propagiert werden (durch den '~'-Operator): Dafür steht zu beiden Seiten des ~ Operators ein Bezeichnerpfad, von denen der Bezeichnerpfad auf der rechten Seite schon an eine Quelle gebunden sein muss. Durch eine solche Konstruktion wird der Bezeichnerpfad auf der linken Seite auch an die gebundenen Quellen des Bezeichnerpfads auf der rechten Seite gebunden. Das folgende Beispiel bringt eine Begründung für ein solches Vorgehen: Beispiel 8 Propagieren gebundener Quellen und Senken … summary FileWriter : Writer { name; 12 Copyright © Fraunhofer IESE 2008 Flow-Anweisungen FileWriter(File file) { this.snk ~ file.filesystem this.name <- file.name } FileWriter(String name) { this.snk ~ sink('FILESYSTEM') this.name <- name this.snk <~ String(name) } } Für den Fall, dass der FileWriter(String name) Konstruktor benutzt wird, wird an das Attribut this.snk eine Senke zum Dateisystem gebunden. Hier ist die Aufgabe des Konstruktors, ein File-Objekt mit dem Namen name zu erzeugen und direkt dahin zu schreiben. Es handelt sich hier um ein Internalisieren von Effekten, die erst im File-Objekt auftauchen würden. Für den anderen Fall, dass der FileWriter(File file) Konstruktor benutzt wird, wird die Senke this.snk des FileWriters mit den Senken des FileObjekts am Attribut file.filesystem verbunden. Man muss hier beachten, dass es sich bei file auch um ein Objekt einer Unterklasse von File handeln kann, welche nicht direkt an die Senke des Dateisystems angebunden ist. Um diese Flexibilität der Objektorientierung zu wahren wird hier der Bindungsoperator verwendet anstatt this.snk direkt an eine Senke zu binden, wie im anderen Konstruktor. 4.3 Flows Flows stehen für einen einfachen, nicht-verändernden Datenfluss vom Bezeichner auf der rechten Seite (dem Ursprung des Flows), zum Bezeichner auf der linken Seite (dem Ziel des Flows). Flow-Klauseln haben folgende Syntax: FLOW RETURNFLOW OUTPUT INPUT ::= ::= ::= ::= OUTPUT <- INPUT <- INPUT PATH FUNCTION | OBJECTGENERATOR | PATH Der Ursprung (INPUT) des Flows kann eine Funktion, eine Objekterzeugung oder ein Bezeichnerpfad sein, während das Ziel (OUTPUT) nur ein Bezeichnerpfad sein darf. Beim RETURNFLOW handelt es sich um den Rückfluss an die aufrufende Methode, daher wird kein Ziel für den Flow-Operator benötigt. Der Flow-Operator unterscheidet sich vom Pipe-Operator, bei dem beide Bezeichner nicht-lokal sind. Siehe dazu Abschnitt 4.5. Copyright © Fraunhofer IESE 2008 13 Flow-Anweisungen Beispiel 9 Spezifizieren von Flüssen … summary String { … String append(String tail){ … tmp <- tail <- tmp } } 4.4 Funktionale Ausdrücke Datenflüsse können unverändert fließen oder auf ihrem Weg signifikante Umformungen erfahren. In Flow werden solche Datenflusstransformationen durch Flow-Funktionen ausgedrückt. Das Auftreten einer Funktion signalisiert, dass die Daten auf ihrem Weg eine bedeutsame Änderung erfahren. Was in diesem Zusammenhang als »bedeutsam« anzusehen ist, hängt von den Analysezielen des Modellierers ab. Da Flow für Sicherheitsanalysen konzipiert wurde, werden derzeit vor allem sicherheitsrelevante Umformungen durch entsprechende Flow-Funktionen repräsentiert. Funktionen haben einen Namen (der die Art der Transformation der Argumente definiert) und eine Argumentliste. FUNCTION INPUTS ANNOTATIONS ANNOTATION VALUESET ::= ::= ::= ::= ::= <function name> [ < ANNOTATIONS > ] ( [INPUTS] ) INPUT [ , INPUTS] ANNOTATION [ , ANNOTATIONS] <attribute name> = ['<value>' | { VALUESET } ] '<value>' [ , VALUESET ] Derzeit sind folgende Funktionen definiert: Tabelle 1 14 Vordefinierte Flow-Funktionen Funktion Bedeutung sanitize<safe='<sink>'>(<origin>) Die Daten aus <origin> werden bzgl. Senken vom Typ <sink> bereinigt, d.h. sie werden so umgeformt, dass sie für den angegebenen Senken-Typ keine Gefahr mehr darstellen. (Ein typischer Bereinigungsoperator ist zum Beispiel das sogenannte Escaping von Steuerzeichen in Nutzdaten.) Copyright © Fraunhofer IESE 2008 Flow-Anweisungen Funktion Bedeutung enable<unsafe='<sink>'>(<origin>) Die (zuvor vielleicht sicheren) Daten aus <origin> werden vor Weitergabe so umgeformt, dass dadurch deren Ungefährlichkeit in Bezug auf Senken des Typs <sink> nicht mehr gewährleistet ist. (Es werden zum Beispiel Escape-Zeichen entfernt, sodass ursprünglich inaktive Steuerzeichen wieder reaktiviert werden.) Die enable-Funktion hebt ein vorangegangenes sanitize wieder auf. split(<origin>) Die Argumentdaten in werden gestückelt weitergegeben. slice(<origin>) Die Argumentdatenwerden in Teilen weitergegeben. chain(<origin1>,<origin2>,…) Es wird eine Kombination von <origin1>, <origin2> ... weitergegeben. (Ein typisches Anwendungsbeispiel ist die Modellierung von StringKonkatenation.) translate(<origin>) Die Daten werden in eine andere Darstellung übersetzt (z.B. Latin1 → UTF8) todata(<origin>) Die Daten werden in einen anderen Datentyp umgewandelt (z.B. '9' → 9) totext(<origin>) Die Daten werden in eine textuelle Darstellung gebracht (9 → '9') Beispiele für die Verwendung dieser Funktionen finden sich im Anhang, siehe Beispiel 14 bis Beispiel 16 ab Seite 21. Dem Flow-Anwender steht es frei, eigene Funktionsnamen mit frei gewählten Annotationen zu verwenden. Der Flow-Compiler interpretiert benutzerdefinierte Funktionen nicht, sondern fügt lediglich die gewählten Bezeichner an den entsprechenden Stellen in den Datenfluss ein. Die Interpretation der Funktionsnamen und Annotationen obliegt der nachgeordneten Datenflussanalyse. Die in Tabelle 1 genannten, vordefinierten Funktionen sind auf das SecFlow- Copyright © Fraunhofer IESE 2008 15 Flow-Anweisungen Analyseframework zugeschnitten und haben dort eine genau festgelegte Semantik. 4.5 Pipes Eine besondere Form des Flow-Operators ist der Pipe-Operator. Syntaktisch entspricht er einem Flow-Operator zwischen zwei nicht lokalen Variablen. Wenn eine Pipe im Analyselauf vorkommt, entsteht eine permanente Verbindung zwischen dem Ursprungs- und dem Zielsausdruck. Im Beispiel bedeutet das, dass beim Aufruf der Methode write zunächst der Inhalt des Parameters str in this.buffer fließt. Wenn die Pipe zwischen file.content und this.buffer bereits erzeugt wurde — durch einen vorhergehenden Aufruf von setFile — so bewirkt ein Fluss in this.buffer auch gleichzeitig einen Fluss nach file.content. Der Effekt einer Pipe ist also nicht lokal beschränkt. Beispiel 10 Verwendung von Pipes … summary FileProxy { … void setFile(File file){ file.content <- this.buffer } void write(String str){ this.buffer <- str } } 4.6 Objekt-Instanziierung Bei der Objekt-Instanziierung wird an dem durch den Zielbezeichner (PATH) angegebenen Konstrukt ein neues Objekt vom Typ Type erzeugt. OBJECTGENERATOR TYPE ARRAYS ::= ::= ::= PATH <- [ TYPE ] <local classname> [ARRAYS] [][ARRAYS] Eine Objekt-Instanziierung in Flow führt dazu, dass nur der Initialisierer des erzeugten Objekts interpretiert wird. Evtl. wurde vorher noch der statische Initialisierer interpretiert. Ein Konstruktor wird in diesem Fall nicht interpretiert. Beispiel 11 Objektinstanziierung … summary String { 16 Copyright © Fraunhofer IESE 2008 Flow-Anweisungen … String append(String tail){ tmp <- [String] tmp <- chain(this, tail) <- tmp } } 4.7 Daten-Generierung/Konsumierung Um auszudrücken, dass Daten einer Quelle gelesen werden, wird der an die Quelle gebundene Bezeichner mit dem Operator <* und einer Typangabe als Ursprung des Datums dargestellt: DATAGENERATOR ::= PATH <* TYPE ( INPUT ) Sollte INPUT nicht an eine Quelle gebunden sein, kommt es dadurch nicht zu einem Fehler. Die Analyse interpretiert das dann als Lesen von einer sicheren Quelle. Um auszudrücken, dass Daten in eine Senke abfließen, lautet die Syntax: DATACONSUMPTION ::= PATH <~ TYPE ( INPUT) Das Schreiben in nicht an Senken gebundene Bezeichner verhält sich analog zum Lesen. Das heißt, alle nicht gebundenen Senken-Bezeichner werden in der Analyse als unkritische Senken interpretiert. Beispiel 12 Spezifizieren eines Datengenerators … Summary FileReader { in … String read() { tmp <- [String] tmp <* String(data) <- tmp } } Beispiel 13 Spezifizieren eines Datenkonsumenten … Summary FileWriter { out … void write(String data) { out <~ String(data) } Copyright © Fraunhofer IESE 2008 17 Flow-Anweisungen } 18 Copyright © Fraunhofer IESE 2008 Tabellenverzeichnis 5 Fazit Die Sprache Flow wurde entwickelt, um Datenflüsse in Programmen zu beschreiben. Die Sprachmittel von Flow sind auf objektorientierte Zielsprachen zugeschnitten, da sich die Datenflussdeklaration auf Klassen und Methoden beziehen. Konkret unterstützt der derzeit verfügbare Flow-Compiler die Programmiersprachen Java und C#. Allerdings wurden die Konzepte der Sprache so allgemein gehalten, dass eine Erweiterung auf andere Sprachen mit vertretbarem Aufwand möglich sein sollte. Um keine Bezeichnungskonflikte mit den Sprachelementen der Zielsprachen heraufzubeschwören, verwendet Flow bewusst eigene, in anderen Sprachen nicht gebräuchliche Operatoren und Beschreibungsmittel, lehnt diese aber an gebräuchliche Syntaxkonventionen an, um das Erlernen der Sprache zu erleichtern. Flow wurde mit dem Ziel konzipiert, sicherheitsrelevante Aspekte von Datenflüssen zu modellieren. Dies spiegelt sich am deutlichsten in den verfügbaren Flow-Funktionen (vgl. Abschnitt 4.4, Tabelle 1) wider, die in besonderem Maße Sicherheitstransformationen (z.B. sanitize, enable) adressieren. Es spricht jedoch nichts dagegen, den Sprachumfang um neue Funktionsbezeichner zu erweitern, um je nach Bedarf auch andere relevante Datenflussaspekte in der Spezifikation zu repräsentieren. Copyright © Fraunhofer IESE 2008 19 Anhang — Anwendungsbeispiele Anhang — Anwendungsbeispiele Im Folgenden sind mehrere Flow-Spezifikation dargestellt, die das Zusammenspiel der Flow-Sprachelemente noch einmal in einem etwas umfassenderen Kontext anhand realer Anwendungsfälle illustrieren. Die Benutzung der verschiedenen Flow Funktionen lässt sich sehr gut an der Flow Repräsentation der Klasse java.lang.String darstellen. Für diese Klasse ist kein Quellcode verfügbar, so dass eine Modellierung mit Flow unerlässlich ist, falls java.lang.String innerhalb des Analysehorizonts verwendet wird. Beispiel 14 Ausschnitt der Flow-Spezifikation der Klasse java.lang.String @'Java' StringBuffer @ [java.lang] Object @ [java.lang] String @ [java.lang] Locale @ [java.util] CharSequence @ [java.lang] summary String : CharSequence, Object { tostring { <- this } String(String value) { this <- value } int codePointAt(int index){ <- translate(slice(this)) } String[] split(String regex){ result <- [String[]] result[] <- split(this) <- result } String copyValueOf(char[] data, int offset, int count){ <- slice(data.tostring) } String format(Locale l, String format, Object[] args){ <- chain(format, args[].tostring) } byte[] getBytes(){ result <- [byte[]] result[] <- split(todata(this)) Copyright © Fraunhofer IESE 2008 21 Anhang — Anwendungsbeispiele result.tostring <- this <- result } String toLowerCase() { <- translate(this) } String valueOf(boolean b) { <- totext(b) } String valueOf(char[] data) { <- data.tostring } String toString() { <- this } } Die Benutzung der Flow Funktionen sanitize und enable wird an der jeweils vollständigen Flow-Spezifikation der Klassen java.net.URLEncoder und java.netURLDecoder ersichtlich: Die Methode encode in java.netURLEncoder encodiert den ersten Parameter gemäß dem Namen der Encodierung im zweiten Parameter, so dass ein String im x-www-form-urlencoded Format entsteht und zurückgegeben wird. Diese Transformation stellt bezüglich der Senke HTTP eine »Entschärfung« des Datums dar — Es kann nun unverändert in eine HTTP Senke, zum Beispiel eine HTTP-Response, eingebettet werden, ohne den Empfänger der Daten in Gefahr zu bringen. Beispiel 15 Flow-Spezifikation der Klasse.java.net.URLEncoder @'Java' URLEncoder @ [java.net] String @ [java.lang] summary URLEncoder { String encode(String string, String enc) { <- sanitize<safe='HTTP'>(translate(string)) } } Die Methode decode in java.netURLEncoder decodiert den ersten Parameter aus dem x-www-form-urlencoded Format gemäß dem Namen der Encodierung im zweiten Parameter. Diese Transformation stellt bezüglich der Senke HTTP eine »Verschärfung« des Datenflusses dar: Die Daten, die eventuell über einen HTTP-Request aus unsicherer Quelle in die Applikation gelangt sind, eignen sich 22 Copyright © Fraunhofer IESE 2008 Anhang — Anwendungsbeispiele nun nicht mehr für eine unveränderte Einbettung in die HTTP-Response (Gefahr von sogenanntem Cross-Site Scripting). Beispiel 16 Flow-Spezifikation der Klasse.java.net.URLDecoder @'Java' URLDecoder @ [java.net] String @ [java.lang] summary URLDecoder { String decode(String string, String enc) { <- enable<unsafe='HTTP'>(translate(string)) } } Copyright © Fraunhofer IESE 2008 23 Abkürzungsverzeichnis Abkürzungsverzeichnis HTTP Copyright © Fraunhofer IESE 2008 Hypertext Transfer Protocol 25 Quellenverzeichnis Quellenverzeichnis [Mandel&Peine 2007] Stefan Mandel, Holger Peine: The SecFlow Source Code Security Checker: Current Problems and Solution Alternatives. 23. Annual Computer Security Application Conference (ACSAC’07), Miami, Florida, Dezember 2007 [Peine et al. 2006] Holger Peine, Stefan Mandel, Dana Richter: SecFlow: Automatische Ermittlung sicherheitskritischer Datenflüsse in Quellcode. Statuskonferenz Forschungsoffensive »Software Engineering 2006«, Juni 2006 [Wagner 2008] Andreas Wagner: Implementierung einer Werkzeugunterstützung zur Spezifikation und Visualisierung von Sicherheitsschwachstellen. Praktikumsbericht, Fraunhofer IESE / Fachhochschule Kaiserslautern, Kaiserslautern, August 2008 Copyright © Fraunhofer IESE 2008 27 Dokument-Information Titel: Flow — Eine DatenflussBeschreibungssprache für objektorientierte Programme Datum: November 2008 Report: IESE-Report Nr. 086.08/D Status: final Klassifikation: public Das vorliegende Dokument entstand im Rahmen des SecFlow-Projekts. Das Verbundprojekt »SecFlow: Automatische Erkennung sicherheitskritischer Datenflüsse in Quellcode« wird vom Bundesministerium für Bildung und Forschung (BMBF) unter dem Förderkennzeichen 01ISF09C gefördert. Copyright 2008, Fraunhofer IESE. Alle Rechte vorbehalten. Diese Veröffentlichung darf für kommerzielle Zwecke ohne vorherige schriftliche Erlaubnis des Herausgebers in keiner Weise, auch nicht auszugsweise, insbesondere elektronisch oder mechanisch, als Fotokopie oder als Aufnahme oder sonst wie vervielfältigt, gespeichert oder übertragen werden. Eine schriftliche Genehmigung ist nicht erforderlich für die Vervielfältigung oder Verteilung der Veröffentlichung von bzw. an Personen zu privaten Zwecken.