Page Menu
Home
c4science
Search
Configure Global Search
Log In
Files
F92580955
render.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Thu, Nov 21, 16:18
Size
9 KB
Mime Type
text/x-php
Expires
Sat, Nov 23, 16:18 (1 d, 23 h)
Engine
blob
Format
Raw Data
Handle
22463590
Attached To
rPHU libphutil
render.php
View Options
<?php
/**
* Render an HTML tag in a way that treats user content as unsafe by default.
*
* Tag rendering has some special logic which implements security features:
*
* - When rendering `<a>` tags, if the `rel` attribute is not specified, it
* is interpreted as `rel="noreferrer"`.
* - When rendering `<a>` tags, the `href` attribute may not begin with
* `javascript:`.
*
* These special cases can not be disabled.
*
* IMPORTANT: The `$tag` attribute and the keys of the `$attributes` array are
* trusted blindly, and not escaped. You should not pass user data in these
* parameters.
*
* @param string The name of the tag, like `a` or `div`.
* @param map<string, string> A map of tag attributes.
* @param wild Content to put in the tag.
* @return PhutilSafeHTML Tag object.
*/
function
phutil_tag
(
$tag
,
array
$attributes
=
array
(),
$content
=
null
)
{
// If the `href` attribute is present:
// - make sure it is not a "javascript:" URI. We never permit these.
// - if the tag is an `<a>` and the link is to some foreign resource,
// add `rel="nofollow"` by default.
if
(!
empty
(
$attributes
[
'href'
]))
{
// This might be a URI object, so cast it to a string.
$href
=
(
string
)
$attributes
[
'href'
];
if
(
isset
(
$href
[
0
]))
{
$is_anchor_href
=
(
$href
[
0
]
==
'#'
);
// Is this a link to a resource on the same domain? The second part of
// this excludes "//evil.com/" protocol-relative hrefs. The third part
// of this excludes "/\evil.com/" protocol-relative fantasy hrefs which
// are completely made up but which browsers all respect. Broadly,
// browsers will dutifuly treat "/" followed by ANY sequence of "/" and
// "\" as though it were "//".
$is_domain_href
=
(
$href
[
0
]
==
'/'
)
&&
(!
isset
(
$href
[
1
])
||
(
$href
[
1
]
!=
'/'
&&
$href
[
1
]
!=
'
\\
'
));
// If the `rel` attribute is not specified, fill in `rel="noreferrer"`.
// Effectively, this serves to make the default behavior for offsite
// links "do not send a referrer", which is broadly desirable. Specifying
// some non-null `rel` will skip this.
if
(!
isset
(
$attributes
[
'rel'
]))
{
if
(!
$is_anchor_href
&&
!
$is_domain_href
)
{
if
(
$tag
==
'a'
)
{
$attributes
[
'rel'
]
=
'noreferrer'
;
}
}
}
// Block 'javascript:' hrefs at the tag level: no well-designed
// application should ever use them, and they are a potent attack vector.
// This function is deep in the core and performance sensitive, so we're
// doing a cheap version of this test first to avoid calling preg_match()
// on URIs which begin with '/' or `#`. These cover essentially all URIs
// in Phabricator.
if
(!
$is_anchor_href
&&
!
$is_domain_href
)
{
// Chrome 33 and IE 11 both interpret "javascript\n:" as a Javascript
// URI, and all browsers interpret " javascript:" as a Javascript URI,
// so be aggressive about looking for "javascript:" in the initial
// section of the string.
$normalized_href
=
preg_replace
(
'([^a-z0-9/:]+)i'
,
''
,
$href
);
if
(
preg_match
(
'/^javascript:/i'
,
$normalized_href
))
{
throw
new
Exception
(
pht
(
"Attempting to render a tag with an '%s' attribute that begins "
.
"with '%s'. This is either a serious security concern or a "
.
"serious architecture concern. Seek urgent remedy."
,
'href'
,
'javascript:'
));
}
}
}
}
// For tags which can't self-close, treat null as the empty string -- for
// example, always render `<div></div>`, never `<div />`.
static
$self_closing_tags
=
array
(
'area'
=>
true
,
'base'
=>
true
,
'br'
=>
true
,
'col'
=>
true
,
'command'
=>
true
,
'embed'
=>
true
,
'frame'
=>
true
,
'hr'
=>
true
,
'img'
=>
true
,
'input'
=>
true
,
'keygen'
=>
true
,
'link'
=>
true
,
'meta'
=>
true
,
'param'
=>
true
,
'source'
=>
true
,
'track'
=>
true
,
'wbr'
=>
true
,
);
$attr_string
=
''
;
foreach
(
$attributes
as
$k
=>
$v
)
{
if
(
$v
===
null
)
{
continue
;
}
$v
=
phutil_escape_html
(
$v
);
$attr_string
.=
' '
.
$k
.
'="'
.
$v
.
'"'
;
}
if
(
$content
===
null
)
{
if
(
isset
(
$self_closing_tags
[
$tag
]))
{
return
new
PhutilSafeHTML
(
'<'
.
$tag
.
$attr_string
.
' />'
);
}
else
{
$content
=
''
;
}
}
else
{
$content
=
phutil_escape_html
(
$content
);
}
return
new
PhutilSafeHTML
(
'<'
.
$tag
.
$attr_string
.
'>'
.
$content
.
'</'
.
$tag
.
'>'
);
}
function
phutil_tag_div
(
$class
,
$content
=
null
)
{
return
phutil_tag
(
'div'
,
array
(
'class'
=>
$class
),
$content
);
}
function
phutil_escape_html
(
$string
)
{
if
(
$string
instanceof
PhutilSafeHTML
)
{
return
$string
;
}
else
if
(
$string
instanceof
PhutilSafeHTMLProducerInterface
)
{
$result
=
$string
->
producePhutilSafeHTML
();
if
(
$result
instanceof
PhutilSafeHTML
)
{
return
phutil_escape_html
(
$result
);
}
else
if
(
is_array
(
$result
))
{
return
phutil_escape_html
(
$result
);
}
else
if
(
$result
instanceof
PhutilSafeHTMLProducerInterface
)
{
return
phutil_escape_html
(
$result
);
}
else
{
try
{
assert_stringlike
(
$result
);
return
phutil_escape_html
((
string
)
$result
);
}
catch
(
Exception
$ex
)
{
throw
new
Exception
(
pht
(
"Object (of class '%s') implements %s but did not return anything "
.
"renderable from %s."
,
get_class
(
$string
),
'PhutilSafeHTMLProducerInterface'
,
'producePhutilSafeHTML()'
));
}
}
}
else
if
(
is_array
(
$string
))
{
$result
=
''
;
foreach
(
$string
as
$item
)
{
$result
.=
phutil_escape_html
(
$item
);
}
return
$result
;
}
return
htmlspecialchars
(
$string
,
ENT_QUOTES
,
'UTF-8'
);
}
function
phutil_escape_html_newlines
(
$string
)
{
return
PhutilSafeHTML
::
applyFunction
(
'nl2br'
,
$string
);
}
/**
* Mark string as safe for use in HTML.
*/
function
phutil_safe_html
(
$string
)
{
if
(
$string
==
''
)
{
return
$string
;
}
else
if
(
$string
instanceof
PhutilSafeHTML
)
{
return
$string
;
}
else
{
return
new
PhutilSafeHTML
(
$string
);
}
}
/**
* HTML safe version of `implode()`.
*/
function
phutil_implode_html
(
$glue
,
array
$pieces
)
{
$glue
=
phutil_escape_html
(
$glue
);
foreach
(
$pieces
as
$k
=>
$piece
)
{
$pieces
[
$k
]
=
phutil_escape_html
(
$piece
);
}
return
phutil_safe_html
(
implode
(
$glue
,
$pieces
));
}
/**
* Format a HTML code. This function behaves like `sprintf()`, except that all
* the normal conversions (like %s) will be properly escaped.
*/
function
hsprintf
(
$html
/* , ... */
)
{
$args
=
func_get_args
();
array_shift
(
$args
);
return
new
PhutilSafeHTML
(
vsprintf
(
$html
,
array_map
(
'phutil_escape_html'
,
$args
)));
}
/**
* Escape text for inclusion in a URI or a query parameter. Note that this
* method does NOT escape '/', because "%2F" is invalid in paths and Apache
* will automatically 404 the page if it's present. This will produce correct
* (the URIs will work) and desirable (the URIs will be readable) behavior in
* these cases:
*
* '/path/?param='.phutil_escape_uri($string); # OK: Query Parameter
* '/path/to/'.phutil_escape_uri($string); # OK: URI Suffix
*
* It will potentially produce the WRONG behavior in this special case:
*
* COUNTEREXAMPLE
* '/path/to/'.phutil_escape_uri($string).'/thing/'; # BAD: URI Infix
*
* In this case, any '/' characters in the string will not be escaped, so you
* will not be able to distinguish between the string and the suffix (unless
* you have more information, like you know the format of the suffix). For infix
* URI components, use @{function:phutil_escape_uri_path_component} instead.
*
* @param string Some string.
* @return string URI encoded string, except for '/'.
*/
function
phutil_escape_uri
(
$string
)
{
return
str_replace
(
'%2F'
,
'/'
,
rawurlencode
(
$string
));
}
/**
* Escape text for inclusion as an infix URI substring. See discussion at
* @{function:phutil_escape_uri}. This function covers an unusual special case;
* @{function:phutil_escape_uri} is usually the correct function to use.
*
* This function will escape a string into a format which is safe to put into
* a URI path and which does not contain '/' so it can be correctly parsed when
* embedded as a URI infix component.
*
* However, you MUST decode the string with
* @{function:phutil_unescape_uri_path_component} before it can be used in the
* application.
*
* @param string Some string.
* @return string URI encoded string that is safe for infix composition.
*/
function
phutil_escape_uri_path_component
(
$string
)
{
return
rawurlencode
(
rawurlencode
(
$string
));
}
/**
* Unescape text that was escaped by
* @{function:phutil_escape_uri_path_component}. See
* @{function:phutil_escape_uri} for discussion.
*
* Note that this function is NOT the inverse of
* @{function:phutil_escape_uri_path_component}! It undoes additional escaping
* which is added to survive the implied unescaping performed by the webserver
* when interpreting the request.
*
* @param string Some string emitted
* from @{function:phutil_escape_uri_path_component} and
* then accessed via a web server.
* @return string Original string.
*/
function
phutil_unescape_uri_path_component
(
$string
)
{
return
rawurldecode
(
$string
);
}
Event Timeline
Log In to Comment