@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
at recaptime-dev/main 187 lines 5.8 kB view raw
1<?php 2 3/** 4 * Render an HTML tag in a way that treats user content as unsafe by default. 5 * 6 * Tag rendering has some special logic which implements security features: 7 * 8 * - When rendering `<a>` tags, if the `rel` attribute is not specified, it 9 * is interpreted as `rel="noreferrer"`. 10 * - When rendering `<a>` tags, the `href` attribute may not begin with 11 * `javascript:`. 12 * 13 * These special cases can not be disabled. 14 * 15 * IMPORTANT: The `$tag` attribute and the keys of the `$attributes` array are 16 * trusted blindly, and not escaped. You should not pass user data in these 17 * parameters. 18 * 19 * @param string $tag The name of the tag, like `a` or `div`. 20 * @param map<string, string> $attributes (optional) A map of tag attributes. 21 * @param mixed $content (optional) Content to put in the tag. 22 * @return PhutilSafeHTML Tag object. 23 */ 24function phutil_tag($tag, array $attributes = array(), $content = null) { 25 // If the `href` attribute is present, make sure it is not a "javascript:" 26 // URI. We never permit these. 27 if (!empty($attributes['href'])) { 28 // This might be a URI object, so cast it to a string. 29 $href = (string)$attributes['href']; 30 31 if (isset($href[0])) { 32 // Block 'javascript:' hrefs at the tag level: no well-designed 33 // application should ever use them, and they are a potent attack vector. 34 35 // This function is deep in the core and performance sensitive, so we're 36 // doing a cheap version of this test first to avoid calling preg_match() 37 // on URIs which begin with '/' or `#`. These cover essentially all URIs 38 // in Phabricator. 39 if (($href[0] !== '/') && ($href[0] !== '#')) { 40 // Chrome 33 and IE 11 both interpret "javascript\n:" as a Javascript 41 // URI, and all browsers interpret " javascript:" as a Javascript URI, 42 // so be aggressive about looking for "javascript:" in the initial 43 // section of the string. 44 45 $normalized_href = preg_replace('([^a-z0-9/:]+)i', '', $href); 46 if (preg_match('/^javascript:/i', $normalized_href)) { 47 throw new Exception( 48 pht( 49 "Attempting to render a tag with an '%s' attribute that begins ". 50 "with '%s'. This is either a serious security concern or a ". 51 "serious architecture concern. Seek urgent remedy.", 52 'href', 53 'javascript:')); 54 } 55 } 56 } 57 } 58 59 // For tags which can't self-close, treat null as the empty string -- for 60 // example, always render `<div></div>`, never `<div />`. 61 static $self_closing_tags = array( 62 'area' => true, 63 'base' => true, 64 'br' => true, 65 'col' => true, 66 'command' => true, 67 'embed' => true, 68 'frame' => true, 69 'hr' => true, 70 'img' => true, 71 'input' => true, 72 'keygen' => true, 73 'link' => true, 74 'meta' => true, 75 'param' => true, 76 'source' => true, 77 'track' => true, 78 'wbr' => true, 79 ); 80 81 $attr_string = ''; 82 foreach ($attributes as $k => $v) { 83 if ($v === null) { 84 continue; 85 } 86 $v = phutil_escape_html($v); 87 $attr_string .= ' '.$k.'="'.$v.'"'; 88 } 89 90 if ($content === null) { 91 if (isset($self_closing_tags[$tag])) { 92 return new PhutilSafeHTML('<'.$tag.$attr_string.' />'); 93 } else { 94 $content = ''; 95 } 96 } else { 97 $content = phutil_escape_html($content); 98 } 99 100 return new PhutilSafeHTML('<'.$tag.$attr_string.'>'.$content.'</'.$tag.'>'); 101} 102 103function phutil_tag_div($class, $content = null) { 104 return phutil_tag('div', array('class' => $class), $content); 105} 106 107function phutil_escape_html($string) { 108 if ($string === null) { 109 return ''; 110 } 111 112 if ($string instanceof PhutilSafeHTML) { 113 return $string; 114 } else if ($string instanceof PhutilSafeHTMLProducerInterface) { 115 $result = $string->producePhutilSafeHTML(); 116 if ($result instanceof PhutilSafeHTML) { 117 return phutil_escape_html($result); 118 } else if (is_array($result)) { 119 return phutil_escape_html($result); 120 } else if ($result instanceof PhutilSafeHTMLProducerInterface) { 121 return phutil_escape_html($result); 122 } else { 123 try { 124 assert_stringlike($result); 125 return phutil_escape_html((string)$result); 126 } catch (Exception $ex) { 127 throw new Exception( 128 pht( 129 "Object (of class '%s') implements %s but did not return anything ". 130 "renderable from %s.", 131 get_class($string), 132 'PhutilSafeHTMLProducerInterface', 133 'producePhutilSafeHTML()')); 134 } 135 } 136 } else if (is_array($string)) { 137 $result = ''; 138 foreach ($string as $item) { 139 $result .= phutil_escape_html($item); 140 } 141 return $result; 142 } 143 144 return htmlspecialchars($string, ENT_QUOTES, 'UTF-8'); 145} 146 147function phutil_escape_html_newlines($string) { 148 return PhutilSafeHTML::applyFunction('nl2br', $string); 149} 150 151/** 152 * Mark string as safe for use in HTML. 153 */ 154function phutil_safe_html($string) { 155 if ($string == '') { 156 return $string; 157 } else if ($string instanceof PhutilSafeHTML) { 158 return $string; 159 } else { 160 return new PhutilSafeHTML($string); 161 } 162} 163 164/** 165 * HTML safe version of `implode()`. 166 */ 167function phutil_implode_html($glue, array $pieces) { 168 $glue = phutil_escape_html($glue); 169 170 foreach ($pieces as $k => $piece) { 171 $pieces[$k] = phutil_escape_html($piece); 172 } 173 174 return phutil_safe_html(implode($glue, $pieces)); 175} 176 177/** 178 * Format a HTML code. This function behaves like `sprintf()`, except that all 179 * the normal conversions (like %s) will be properly escaped. 180 */ 181function hsprintf($html /* , ... */) { 182 $args = func_get_args(); 183 array_shift($args); 184 return new PhutilSafeHTML( 185 vsprintf($html, array_map('phutil_escape_html', $args))); 186} 187