Security Analysis in Hakana
September 23, 2024 · View on GitHub
Hakana can attempt to find connections between user-controlled input (like HH\global_get('GET')['name']) and places that we don’t want unescaped user-controlled input to end up (like echo "<h1>$name</h1>" by looking at the ways that data flows through your application (via assignments, function/method calls and array/property access).
You can enable this mode by running <hakana path> security-check. When taint analysis is enabled, no other analysis is performed.
Tainted input is anything that can be controlled, wholly or in part, by a user of your application. In taint analysis, tainted input is called a taint source.
Example sources:
$_GET[‘id’]$_POST['email']$_COOKIE['token']
Taint analysis tracks how data flows from taint sources into taint sinks. Taint sinks are places you really don’t want untrusted data to end up.
Example sinks:
<form action={$endpoint}>...</form>$conn->query("select * from users where name='$name'")
Taint Sources
Hakana recognises a number of taint sources, defined in the SourceType enum class:
UriRequestHeader- any data that comes from a given request‘s URINonUriRequestHeader- any data that comes from the non-URI component of a request (like POST data)RawUserData- any data that comes from persistently-stored data (i.e. longer-lived than a single request) that can be controlled by a user. This must be explicitly annotated in your application with Hakana attributesUserData- any data that belongs to a specific userUserEmail- any user's emailUserPII— any data that contains Personally-Identifiable Information. This must be explicitly annotated in your application with Hakana attributes.UserPassword— any data that contains user secrets (e.g. hashed password fields). This must be explicitly annotated in your application with Hakana attributes.SystemSecret— any data that contains system secrets (e.g. API keys). This must be explicitly annotated in your application with Hakana attributes.
Taint Sinks
Hakana recognises a range of taint sinks, defined in the SinkType enum
HtmlTag- used for anywhere that emits arbitrary HTMLSql- used for anywhere that can execute arbitrary SQL stringsShell- used for anywhere that can execute arbitrary shell commandsFileSystem- used for any read/write to an arbitrary file pathRedirectUri- used for anywhere that redirects to an arbitrary URIUnserialize- used for anywhere that unserializes arbitrary dataCookie- used for anywhere that saves arbitrary cookie informationCurlHeader- used for anywhere that sends arbitrary header information in a Curl requestCurlUri- used for anywhere that sends Curl requests to an arbitrary URIHtmlAttribute- used for anywhere that emits arbitrary HTML attributesHtmlAttributeUri- used for anywhere that emits arbitrary URIs embedded in HTML codeLogging- used for anywhere that logs arbitrary stringsOutput- used for anywhere thatechos arbitrary strings
Annotating your code for security analysis
Hakana understands a number of existing Hack sinks and sources — for example, it knows that the first argument of AsyncMysqlConnection::query is a Sql taint sink.
You may want to add more annotations — for example, annotations are necessary to describe UserPII sources.
Hakana comes with built-in support for security analysis attributes that allow to annotate your code appropriately.
Hakana\SecurityAnalysis\IgnorePath
Use this attribute for a function or method that can never be executed in a production context.
Hakana\SecurityAnalysis\IgnorePathIfTrue
Use this attribute for any function or method that determines whether the execution context is production or not.
<<Hakana\SecurityAnalysis\IgnorePathIfTrue()>>
function is_dev(): bool {
...
}
function foo(): void {
if (is_dev()) {
$a = $_GET['a'];
echo $a; // this is fine
}
$a = $_GET['a'];
echo $a; // this gets flagged
}
Hakana\SecurityAnalysis\RemoveTaintsWhenReturningTrue
Use this attribute for any function or method that checks whether a value is "safe".
function is_valid_countrycode(
<<Hakana\SecurityAnalysis\RemoveTaintsWhenReturningTrue('HtmlTag')>>
string $country_code
): bool {
...
}
function foo(): void {
$a = $_GET['a'];
if (is_valid_countrycode($a)) {
echo $a;
}
}
Hakana\SecurityAnalysis\Sanitize
Use this attribute for any function or method that sanitizes its input in a manner that Hakana cannot understand.
<<\Hakana\SecurityAnalysis\Sanitize('HtmlTag')>>
function custom_html_escape(string $arg): string {
...
}
$tainted = $_GET['foo'];
echo custom_html_escape($tainted);
Hakana\SecurityAnalysis\ShapeSource
Given a type alias that defines a shape, you can use ShapeSource to define per-field source types.
<<\Hakana\SecurityAnalysis\ShapeSource(dict[
'password' => 'UserPassword'
])>>
type user_t = shape(
'id' => int,
'username' => string,
'password' => string,
);
function takesUser(user_t $user) {
echo $user['username']; // this is ok
echo $user['email']; // this is an error
}
Hakana\SecurityAnalysis\Sink
Use this attribute on any function or method params that you want to be considered as sinks in Hakana. You can pass the name of the taint sink as a string.
function fetch(int $id): string {
return db_query("SELECT * FROM table WHERE id=" . $id);
}
function db_query(
<<\Hakana\SecurityAnalysis\Sink('Sql')>>
string $sql
): string {
..
}
$value = $_GET["value"];
$result = fetch($value);
Hakana\SecurityAnalysis\Source
Use this attribute on any function of method that you want to be considered a source in Hakana. You can pass the name of the taint source as a string.
class User {
<<\Hakana\SecurityAnalysis\Source('UserPII')>>
public function getEmail() : string {
...
}
}
function takesUser(User $user): void {
echo $user->getEmail(); // this is an error
}
HAKANA_SECURITY_IGNORE
In addition to attributes, Hakana supports using the HAKANA_SECURITY_IGNORE[<SinkType>] doc comment to suppress individual paths. This can be used when you want to deliberately do something that would otherwise be considered dangerous.
function check_endpoint(string $s): void {
// make curl req to see if a given endpoint is valid
}
function explictly_follow_user_uri(): void {
$endpoint = $_POST['endpoint'];
check_endpoint(
/* HAKANA_SECURITY_IGNORE[CurlUri] stops taint analysis through this path */
$endpoint
);
}