一部のHTMLタグを通すフィルタ
Posted feedbacks - Flatten
Nested Hiddenタグを無効化するために「<」のサニタイズするのなら 「;」も対象にしたほうが良い予感?
TripletaiL フレームワークには一部のタグだけを許可する機能があります.
ただ,題意のサンプルのように,属性値を '' で括った場合は
うまく対応できないので,そこを変更しています.
また,閉じタグの自動修復機能が少しバグっていたので
以下のパッチを当てて実行しています.
--- lib/Tripletail/TagCheck.pm 27 Jun 2007 03:01:50 -0000 1.15
+++ lib/Tripletail/TagCheck.pm 31 Aug 2007 01:03:31 -0000
@@ -253,7 +253,9 @@
}
# スタックにプッシュ
- push @$open_stack, $elem;
+ if(!$taginfo->mustBeEmpty) {
+ push @$open_stack, $elem;
+ }
}
}
}
----
実行結果
<<<<
<a href="www.google.com">link</a> <blink>and</blink>
<strong onClick='alert("NG")'>click<br>me!</strong>
>>>>
<a href="www.google.com">link</a> <blink>and</blink>
<strong>click<br>me!</strong>
see: TripletaiL TagCheck のマニュアル
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #!/usr/bin/perl
use Tripletail qw(/dev/null);
my $input = q{
<a href="www.google.com">link</a> <blink>and</blink>
<strong onClick='alert("NG")'>click<br>me!</strong>
};
my $tc = $TL->newTagCheck;
$tc->setATarget(undef);
$tc->setAllowTag(':BR;A(HREF,NAME);STRONG()');
my $output = $tc->check($input);
print "<<<<\n";
print $input;
print ">>>>\n";
print $output;
|
validityチェックなしの手抜きですが。。。 Tag Soup というライブラリを使う
see: Tag Soup
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | import Data.Char
import Text.HTML.TagSoup
proc :: Tag Char -> Tag Char
proc tag@(TagOpen t attrs)
| t' == "a" = TagOpen t (filter allowA attrs)
| t' == "br" = TagOpen t []
| t' == "strong" = TagOpen t []
| otherwise = tag
where t' = map toLower t
proc tag = tag
allowA (a,_) = a == "href" || a == "name"
pprTags :: [Tag Char] -> String
pprTags [] = ""
pprTags tags@(TagOpen s attrs : TagClose e : ts)
| map toLower s == "br" = "<br/>" ++ pprTags ts
pprTags (t:ts) = pprTag t ++ pprTags ts
pprTag :: Tag Char -> String
pprTag tag = case tag of
TagOpen t attrs | ignore t -> "<"++pprOpen' t attrs++">"
| otherwise -> pprOpen t attrs
TagClose t | ignore t -> "<"++pprClose' t++">"
| otherwise -> pprClose t
TagText s -> s
TagComment s -> "<!--"++s++"-->"
TagSpecial s c -> "<!"++s++' ':c++">"
TagWarning s -> ""
pprOpen' t [] = t
pprOpen' t attrs = t++' ':unwords (map pprAttr attrs)
pprOpen t attrs = "<" ++ pprOpen' t attrs ++">"
pprClose' t = t
pprClose t = "</" ++ t ++ ">"
pprAttr (a,v) = a++"='"++v++"'"
ignore :: String -> Bool
ignore t = notElem (map toLower t) ["a","strong","br"]
testdata="<a href='www.google.com'>link</a> <blink>and</blink> <strong onClick='alert(\"NG\")'>click<br/>me!</strong>"
-- main = putStrLn . pprTags . map proc . parseTags =<< getContents
main = putStrLn . pprTags . map proc . parseTags $ testdata
{-
*Main> :main
<a href='www.google.com'>link</a> <blink>and<blink> <strong>click<br/>me!</strong>
-}
|
ライブラリを使ったら面白くないので、自分で書いてみた。思ったよりすっきりかけた気がする。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | import java.util.regex._
class ExtendedString(self:String) {
def gsub(reg:Pattern, f:(Matcher)=>String):String = {
val result = new StringBuffer
val m = reg.matcher(self)
while(m.find) m.appendReplacement(result, f(m))
m.appendTail(result)
result.toString
}
def gsub(reg:String, f:(Matcher)=>String):String = gsub(Pattern.compile(reg), f)
}
implicit def string2ext(self:String) = new ExtendedString(self);
object htmlEscape{
lazy val tagRegex = Pattern.compile(
"""(</?)([^"'< >]*)([^"'<>]*(?:"[^"]*"[^"'<>]*|'[^']*'[^"'<>]*)*)((?:>|(?=<)|$(?!\n)))"""
)
lazy val attrRegex = Pattern.compile(
"""[\s'"](\w+)\s*=\s*([^\s'">]+|'[^']+'|\"[^"]+")"""
, Pattern.DOTALL | Pattern.CASE_INSENSITIVE)
lazy val tagAllowed = Set("a", "br", "strong")
lazy val attrAllowed = Map("a" -> Set("href", "name"))
def apply(html:String) = {
html.gsub(tagRegex, (m:Matcher) => {
val tag = m.group(2).toLowerCase.replace("/","")
(if(tagAllowed.contains(tag)){
val attrs = m.group(3).gsub(attrRegex, (m2:Matcher) => {
if(attrAllowed.getOrElse(tag, Set[String]()).contains(m2.group(1).toLowerCase)) {
m2.group(0)
}else {
""
}
})
List(m.group(1),m.group(2), attrs, m.group(4))
}else {
List(m.group(1).replace("<", "<"), m.group(2), m.group(3), m.group(4))
}).mkString("")
})
}
}
println(htmlEscape("""<a href='www.google.com'>link</a> <blink>and</blink> <strong onClick='alert("NG")'>click<br/>me!</strong>"""))
|
正規表現置換でごり押し。 可読性最悪ですみません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | var input = "<a href='www.google.com' name='hoge' title=\"fuga\">\
link</a><blink>and</blink> <strong onClick='alert(\"NG\")'>click<br/>me!</strong>";
var reg = /<\s*(a|strong|br)((?:\s+\w+\s*=\s*(["']).*?\3)*)?\s*(?:>(([^<]*|<[^<>]*[^\/]>|<[^<>]*\/>)*)<\/s*\1\s*>|\/>)/gmi;
function deleteAttr(attr) {
return attr.replace(/\s+(\w+)\s*=\s*(["'])(.*?)\2/g, function(all, name, q, value) {
if(!name.match(/name|href/i)) return '';
else return all;
});
}
function eacapeTags(str) {
var escaped = [];
str = str.replace(reg, function(all, tag, attr, q, inner) {
attr = tag.toUpperCase() == 'A' ? deleteAttr(attr) : '';
escaped .push(tag+ " " + attr, inner);
return "<A/>";
}).replace(/</g, "<");
str = str.replace(/<A\/>/g, function() {
var tag = escaped.shift(), inner = escaped.shift();
if(inner) return "<" + tag + ">" + eacapeTags(inner) + "</" + tag + ">";
else return "<" + tag + "/>";
});
return str;
}
document.body.innerHTML=(eacapeTags(input));
|
inとoutにWSHを使ってます。 cscript "javascriptファイル" フィルタしたい文字列 で実行可能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | if (WScript.Arguments.length != 1)
WScript.Quit();
var target = WScript.Arguments.item(0);
var ret = target.replace(
/<((\/?)([a-z]+)(.*?)(\/?))>/ig,
function(all, ins, head, tag, elms, tail){
switch (tag.toUpperCase())
{
case "A":
var filterElms = elms.match(/ ?(href|name) *= *[^ \/>]+/ig);
var newElement = "";
if (filterElms)
for(var i = 0; i < filterElms.length; i++)
newElement += filterElms[i];
return "<" + head + tag + newElement + tail +">";
case "BR":
case "STRONG":
return "<" + head + tag + tail + ">";
default:
return "<" + ins + ">";
break;
}
});
WScript.Echo(ret);
|
それはなぜですか?
エンティティの無効化です。 このお題で必要かと問われると怪しいですけど、 きっとタグが有効になると何か影響でちゃうのですよね? タグが特別なものであるとしてとらえると タグを無効化する必要のある処理であるならば エンティティも一緒に無効化しても良いかなと。 個人的な好みが強いですが片手落ちというイメージが。。。
1 2 3 4 5 6 7 8 9 10 11 12 13 | def filter(html)
html.gsub(%r!<((/?)([a-z]+)[^>/]*(/?)>)!i){
rest, endtagp, tagname, slash = Regexp.last_match.captures
case tagname
when "br", "BR", "strong", "STRONG"
"<#{endtagp}#{tagname}#{slash}>"
when "a", "A"
endtagp.empty? ? "<a #{$&.scan(/(?:href|name)=(?:".+?"|'.+?'|[^ >]+)/).join ' '}>" : '</a>'
else
"<#{rest}"
end
}
end
|
ちょっと無駄に難しく作りすぎました。 開きタグと閉じタグは個別に扱っても構わなかったのか。 ところで、(要件に含まれるのかわかりませんが)属性に">"が含まれるケースを考慮してない回答が結構ありますね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | var input = "<a title=\"(>_<;)\" href='www.google.com' name='hoge'>\
link</a><blink>and</blink> <strong onClick='alert(\"NG\")'>click<br/>me!</strong>";
function deleteAttr(attr) {
return attr.replace(/\s+(\w+)\s*=\s*(["'])(.*?)\2/g, function(all, name, q, value) {
return name.match(/name|href/i) ? all : '';
});
}
function filter(html) {
return html.replace(/<(\/?)(\w+)((?:\s+\w+\s*=\s*(["']).*?\4)*)?(\/?)>/gmi,
function(all, fslash, tag, attrs, q, rslash) {
switch(tag.toUpperCase()) {
case 'STRONG' : // drop through
case 'BR' : attrs = ''; break;
case 'A' : attrs = deleteAttr(attrs); break;
default : return all.replace('&', '&').replace('<', '<');
}
return '<' + fslash + tag + attrs + rslash + '>';
});
}
document.body.innerHTML=filter(input);
|
htmlって曖昧だから、あまり自信なし。例えば、 <a href=foo.com> みたいに属性がクオートされてない場合はサポートしていない。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import re
def filter(html):
def repl(m):
content = m.group(1)
m = re.match('(/?)\s*(a|br|strong)(\s.*|(/?))', content, re.I + re.S)
if m:
beg_slash, tag, other, end_slash = m.groups()
def attrs(*names):
yield ""
for m in re.finditer(r"""(\S+)\s*=\s*(['"]).+?\2""", other, re.S):
if m.group(1).lower() in names:
yield m.group(0)
def combine(*names):
return "<%s%s%s%s>" % (beg_slash, tag, " ".join(attrs(*names)), end_slash or "")
if tag.lower() == "a":
return combine("href", "name")
else:
return combine()
return "<" + content + ">"
return re.compile('<([^>]*)>', re.S).sub(repl, html)
def main():
print filter("""<a href='www.google.com'>link</a> <blink>and</blink> <strong onClick='alert("NG")'>click<br/>me!</strong>""")
if __name__ == '__main__':
main()
|
属性値の中の">"が考慮されていませんね。 <a href='www.g>oogle.com'>link</a> というようなケースで <a>oogle.com'>link</a> となってしまいます。
修正。
1 2 3 4 5 6 7 8 9 10 11 12 13 | def filter(html)
html.gsub(%r!<((/?)([a-z]+)[^>/]*(/?)>)!i){
rest, endslash, tagname, slash = Regexp.last_match.captures
case tagname.downcase
when "br", "strong"
"<#{endslash}#{tagname}#{slash}>"
when "a"
endslash.empty? ? "<a #{$&.scan(/(?:href|name)=(?:".+?"|'.+?'|[^ >]+)/i).join ' '}>" : '</a>'
else
"<#{rest}"
end
}
end
|
属性をパースする正規表現が二度出てくるのが美しくないですが・・・このあたりをPythonでうまく処理する方法ってあるのかな。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | import re
def filter(html):
def repl(m):
beg_slash, tag_name, attrs, _, end_slash = m.groups()
def combine(*names):
s = "<" + beg_slash + tag_name
for m in re.finditer(r"""(\w+)\s*=\s*(["']).+?\2""", attrs, re.S):
if m.group(1).lower() in names:
s += " " + m.group(0)
return s + end_slash + ">"
if tag_name.lower() == 'a':
return combine("href", "name")
elif tag_name.lower() in ('br', 'strong'):
return combine()
else:
return "<" + m.group(0)[1:]
return re.compile(r"""<(/?)(\w+)((?:\s*\w+\s*=\s*(["']).+?\4)*)\s*(/?)>""", re.S).sub(repl, html)
def main():
print filter("""<a href='www.google.com'>link</a> <blink>and</blink> <strong onClick='alert("NG")'>click<br/>me!</strong>""")
if __name__ == '__main__':
main()
|
br以外の通したタグの閉じ忘れにも対応してみた。 (閉じる順番が間違ってる場合は今回は大目に見るとして。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | <?php
function safehtml($str)
{
$r=array();
$tags=array();
$offs=0;
while(preg_match('!<\s*(/|)\s*(([^>\'"]+|\'[^\']*\'|"[^"]*")*)>!',$str,$m1,PREG_OFFSET_CAPTURE,$offs))
{ $r[]=substr($str,$offs,$m1[0][1]-$offs);
$offs=$m1[0][1]+strlen($m1[0][0]);
preg_match_all('!([a-z0-9_]+)(\s*=\s*("[^"]*"|\'[^\']*\'|[^\s]+)|)!im',$m1[2][0],$m2,PREG_SET_ORDER);
switch(strtolower($m2[0][1]))
{
case 'a':
case 'strong':
if($m1[1][0])
{ if(($i=array_search($m2[0][1],$tags))!==false)
unset($tags[$i]);
}
else
array_unshift($tags,$m2[0][1]);
$t=array($m1[1][0].$m2[0][1]);
if(strtolower($m2[0][1])=='a' && !$m1[1][0])
{ array_shift($m2);
while($param=array_shift($m2))
{ switch(strtolower($param[1]))
{
case 'href':
case 'name':
$t[]=$param[0];
break;
}
}
}
$r[]='<'.implode(" ",$t).'>';
break;
case 'br':
$r[]='<br/>';
break;
default:
$r[]='<'.$m1[1][0].$m1[2][0].'>';
break;
}
}
$r[]=substr($str,$offs);
while($tag=array_shift($tags)) // 閉じわすれタグを閉じる
$r[]="</$tag>";
return implode("",$r);
}
echo safehtml(<<<EOT
<a href='www.google.com' target=_blank>link</a> <blink>and</blink> <strong onClick='alert("NG")'>click<br/>me!</strong>
EOT
);
?>
|
oceanさんのコードで確かめたのでここにぶら下げますが、他のポストにも同じ問題があるかもしれません。
入力文字列が次のようなものだとまずいのでは:
<z foo='<script>alert("Boo")</script>'>
>http://ja.doukaku.org/comment/2722/ 失念してました。 文中に">"等が入っても通るように正規表現修正。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | if (WScript.Arguments.length != 1)
WScript.Quit();
var target = WScript.Arguments.item(0);
var ret = target.replace(
/<(\/?)(\w+)((\s*|\w+|\w+\s*=\s*('[^']*'|"[^"]*"|\w+))*)(\/?)>/ig,
function(all, head, tag, attr, nouse1, nouse2, tail){
switch (tag.toUpperCase())
{
case "A":
var attrs = attr.match(/\s?(href|name)\s*=\s*('[^']*'|"[^"]*"|\w+)/ig);
var newAttr = "";
if (attrs)
for(var i = 0; i < attrs.length; i++)
newAttr += attrs[i];
return "<" + head + tag + newAttr + tail +">";
case "BR":
case "STRONG":
return "<" + head + tag + tail + ">";
default:
return "<" + head + tag + attr + tail + ">";
}
});
WScript.Echo(ret);
|
あ、いけね。開いたタグと閉じたタグで大文字小文字が合ってないと閉じ忘れ扱いになってしまう。 かさばるので修正版は続編のときに…。
というわけで再修正パッチです。でもきっとまだあるな・・・
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | --- a.py.orig Fri Aug 31 16:20:21 2007
+++ a.py Fri Aug 31 16:20:35 2007
@@ -14,11 +14,12 @@
elif tag_name.lower() in ('br', 'strong'):
return combine()
else:
- return "<" + m.group(0)[1:]
+ return m.group(0).replace("<", "<")
return re.compile(r"""<(/?)(\w+)((?:\s*\w+\s*=\s*(["']).+?\4)*)\s*(/?)>""", re.S).sub(repl, html)
def main():
print filter("""<a href='www.google.com'>link</a> <blink>and</blink> <strong onClick='alert("NG")'>click<br/>me!</strong>""")
+ print filter(""" <z foo='<script>alert("Boo")</script>'>""")
if __name__ == '__main__':
main()
|
javascript:document.body.innerHTML=(function(s,r,x){r=/\b(?:name|href) *= *(?:".*?"|'.*?'|[^ >]*)/gi;x=/^\/?(a)|br|strong\b/i;return s.replace(/<([^ >]+ ?)((?:".*?"|'.*?'|[^\/>])*)/g,function(m,t,a){return x.test(t)?'<'+t+(RegExp.$1&&(m=a.match(r))?m.join(' '):''):'<'+t+a})})(document.body.innerHTML)
1 2 3 4 5 6 7 8 | function doukaku54(s){
var rx_nh = /\b(?:name|href) *= *(?:".*?"|'.*?'|[^ >]*)/gi;
var rx_ok = /^\/?(a)|br|strong\b/i, LT = '<', lt = '<', sp = ' ';
return s.replace(/<([^ >]+ ?)((?:".*?"|'.*?'|[^\/>])*)/g, function(m, tag, ats){
return rx_ok.test(tag)
? LT + tag + (RegExp.$1 && (m = ats.match(rx_nh)) ? m.join(sp) : '')
: lt + tag + ats });
}
|
素直にライブラリを使いましたが、お題によれば無効にしたタグ部分は 置き換えた文字以外、そのままにすべきなんでしょうが (例えば大文字小文字など)、ライブラリの都合上、end側タグまわりの情報が劣化しています(27行目あたり)。 これは基底クラスのソース見てなんとかする必要がありそうなので とりあえず手抜き版として投稿させていただきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | from HTMLParser import HTMLParser
class HTMLParser2(HTMLParser):
def reset(self):
HTMLParser.reset(self)
self.buf = ''
def handle_starttag(self, tag, attrs):
if tag == 'a':
self.buf += '<a '+' '.join(["%s='%s'" % t for t in attrs if t[0] in ['href', 'name']])+'>'
elif tag in ['br', 'strong']:
self.buf += '<%s>' % tag
else:
self.buf += self.get_starttag_text().replace('<', '<')
def handle_startendtag(self, tag, attrs):
if tag == 'br':
self.buf += '<br/>'
else:
self.buf += self.get_starttag_text().replace('<', '<')
def handle_endtag(self, tag):
if tag in ['a', 'br', 'strong']:
self.buf += '</%s>' % tag
else:
self.buf += '</%s>' % tag
def handle_data(self, data):
self.buf += data
s = """<a href='www.google.com'>link</a> <blink>and</blink> <strong onClick='al
ert("NG")'>click<br/>me!</strong>"""
h = HTMLParser2()
h.feed(s)
print h.buf
|
#2727の指摘に対応。 タグの大文字小文字の問題修正と通すタグに関する情報をまとめて汎用化してみた。 タグは開いたのと逆順に閉じることを強制する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | <?php
function safehtml($str)
{
$safetag=array('a'=>array(1,array('href','name')),'strong'=>array(1),'br'=>array(0));
$r=array();
$tags=array();
$offs=0;
while(preg_match('!<(\s*(/|)\s*(([^>\'"/]+|\'[^\']*\'|"[^"]*")*)(/|)\s*)>!',$str,$m1,PREG_OFFSET_CAPTURE,$offs))
{ $r[]=substr($str,$offs,$m1[0][1]-$offs);
$offs=$m1[0][1]+strlen($m1[0][0]);
preg_match_all('!([^\s\'"=]+)(\s*=\s*("[^"]*"|\'[^\']*\'|[^\s]+)|)!im',$m1[3][0],$m2,PREG_SET_ORDER);
$tag=strtolower($m2[0][1]);
if(isset($safetag[$tag]))
{ if($safetag[$tag][0]&1)
{ if($m1[2][0])
{ if(array_search($tag,$tags)===false)
continue; // 開いてないタグは閉じない
while(($t=array_shift($tags))) // 開いたのと逆順に閉じる
{ $r[]="</$t>";
if($t==$tag)
break;
}
continue;
}
if(!$m1[5][0])
array_unshift($tags,$tag);
}
$t=array($tag);
if(isset($safetag[$tag][1]) && !$m1[2][0])
{ array_shift($m2);
while($param=array_shift($m2))
{ if(array_search(strtolower($param[1]),$safetag[$tag][1])!==false)
$t[]=$param[0];
}
}
$r[]='<'.$m1[2][0].implode(" ",$t).$m1[5][0].'>';
}
else
$r[]=str_replace(array('<','>'),array('<','>'),$m1[0][0]);
}
$r[]=substr($str,$offs);
while(($tag=array_shift($tags))) // 閉じわすれタグを閉じる
$r[]="</$tag>";
return implode("",$r);
}
echo safehtml(<<<EOT
<a href='www.google.com' target=_blank>link</a> <blink dummy='<'>and</blink> <strong onClick='alert("NG")'>click<br/>me!</strong> <z foo='<script>alert("Boo")</script>'>
EOT
);
?>
|
そういえば <z foo='<a href='www.google.com'>link</a>'> みたいなのは全部エスケープしちゃいますけどそれでいいですよね…
>属性に">"が含まれるケースを考慮してない回答が結構ありますね。
ちなみに僕はどのコードがダメなのか公表してしまった方が(そのコードを書いた人が将来その罠に気づかずに脆弱性のあるプログラムを書いてしまうよりは)いいと思っているので、その手の指摘はたいがいプラス評価しています。
はい。 お題に追記してみました。
なるほど。エンティティや実体参照を許すとどういう脆弱性につながるのかは寡聞にして知りませんが、セミコロンをエスケープしても要件を満たしていそうなので気になるのならそうしてもいいと思います。
ちなみに♥のセミコロンを&semicolon;に置き換えてもハートマークは表示されるみたいですね。
このサイトはセミコロンをエスケープしていませんが、アンパサンドはエスケープしています。
横に長く貫通しているのはpreを使っているせいなのかと思ったら、単に空白が間に入ってないから改行されないだけなんですねぇ。 とりあえずスクロールバーをつけてみました。
この程度の処理だと大して効果が無いようなので,文字列のキャッシュはやめにした。
javascript:with(document.body)(function(s,r,g,x){r=/\b(?:name|href) *= *(?:".*?"|'.*?'|[^ >]*)/gi;g=/</g;x=/^<\/?(?:(a)|br|strong)\b/i;innerHTML=s.replace(/(<[^ >]+ ?)((?:".*?"|'.*?'|[^>])*?)(?=\/?>)/g,function(m,t,a){return x.test(t)?t+(RegExp.$1&&(m=a.match(r))?m.join(' '):''):m.replace(g,'<')})})(innerHTML)
1 2 3 4 5 6 7 8 | function doukaku54(s){
var rx_nh = /\b(?:name|href) *= *(?:".*?"|'.*?'|[^ >]*)/gi,
rx_ok = /^<\/?(?:(a)|br|strong)\b/i, rx_lt = /</g;
return s.replace(/(<[^ >]+ ?)((?:".*?"|'.*?'|[^>])*?)(?=\/?>)/g, function(m, tag, ats){
return rx_ok.test(tag)
? tag + (RegExp.$1 && (m = ats.match(rx_nh)) ? m.join(' ') : '')
: m.replace(rx_lt, '<') });
}
|
きゅ~勘違いしてました。 「;」のエンティティって存在しないみたいです。 「&」で十分です。 昔、エンティティを無効にするときは「;」というのを読んだ気がしたんだけど、うーんソースが見つからない。 なんでエンティティを無効にしないといけなかったんだっけな~? 何か理由があったはずなんだけど。
バグありました… orz
1 2 3 4 5 | @@ -15,3 +15,3 @@
case 'A' : attrs = deleteAttr(attrs); break;
- default : return all.replace('&', '&').replace('<', '<');
+ default : return all.replace(/&/g, '&').replace(/</g, '<');
}
|
see: いっそこの欄を…
些細な突っ込みですが… HTMLだとするとサンプルあった“<br/>”は“<br/>”にしなきゃいけないんじゃないんでしょうか? 無論“<br>”は“<br>”として
あ、なるほど、IEだと折り返されているんですねぇ…。 PREで囲っちゃうとFirefoxではOKになったけども IEでは逆に突き抜けるように…うむむ。 とりあえずPREは外しておきます。
コードとコメント本文に対して下のスクリプトを適用するといいのかも…。
うむむ。「一部の属性を生かす必要があるタグ」「閉じタグのあるタグ」「閉じタグのないタグ」という選定のつもりでしたが、確かに<br/>がエスケープされるべきかどうか明言しておいた方がよかったですね。今回はどちらの解答でも題意を満たしているものとします。
「日記サービスでユーザに一部のHTMLを許可する」というユースケースをイメージして作ったお題なので、続編からは「<br/>も<br>も両方許容せよ」というようになると思います。
1. パーザ (文字列→構文木) 2. トランスレータ (構文木→構文木) 3. プリンタ (構文木→文字列) と分けて パーザは Tag Soupライブラリのものを使い、 タグのエスケープはトランスレータで行うようにし、 プリンタは自前で書いてみました。 コメントは保存されます。 こちらの方が前の投稿のものよりモジュラリティが 高くなった気がします。 urlエンコーディングによるuriの値表現の validatingは手を抜いておこなっていません。 (元元要求にはなかったような気がしますと言い訳してみる:p)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | module Main (main) where
import Data.Char
import Data.Maybe
import Text.HTML.TagSoup
-- Parsing
-- Text.HTML.TagSoup.parseTags :: String -> [Tag Char]
-- Translating
translate :: [Tag Char] -> [Tag Char]
translate = map trans
trans :: Tag Char -> Tag Char
trans tag = case tag of
TagOpen t attrs | ignore t -> escapeTagOpen t attrs
| otherwise -> TagOpen t (filterAttr t attrs)
TagClose t | ignore t -> escapeTagClose t
_ -> tag
ignore :: String -> Bool
ignore = flip notElem (map fst filterTable) . map toLower
escapeTagOpen t attrs
= TagText $ "<"++t++escape (' ':unwords (map showAttr attrs))++">"
escapeTagClose t
= TagText $ "</"++t++">"
filterAttr :: String -> [Attribute Char] -> [Attribute Char]
filterAttr t = filter ((maybe (const True) id (lookup t filterTable)) . fst)
filterTable :: [(String,String->Bool)]
filterTable = [("a",flip elem ["href","name"])
,("strong",const False)
,("br",const False)]
-- Showing
showTags :: [Tag Char] -> String
showTags [] = ""
showTags (TagOpen s attrs : TagClose e : ts) | isEmptyTag e
= angle (s ++ ' ':unwords (map showAttr attrs)++" /")++showTags ts
showTags (t:ts)
= showTag t ++ showTags ts
showTag tag = case tag of
TagOpen t attrs -> angle $ t ++ ' ':unwords (map showAttr attrs)
TagClose t -> angle $ t ++ "/"
TagText s -> s
TagComment c -> angle $ "!--" ++ c ++ "--"
TagSpecial s t -> angle $ "!" ++ s ++ ' ':t
TagWarning s -> ""
angle :: String -> String
angle s = "<"++s++">"
isEmptyTag :: String -> Bool
isEmptyTag = flip elem ["br","hr"] -- not full fledged
showAttr :: Attribute Char -> String
showAttr (a,v) = a ++ "=" ++ q v
where q v = if elem sq v then dq:v++[dq]
else sq:v++[sq]
sq = '\''
dq = '\"'
escape :: String -> String
escape = concatMap esc
where esc '<' = "<"
esc '>' = ">"
esc '&' = "&"
esc c = [c]
--
main :: IO ()
-- main = putStrLn . showTags . translate . parseTags =<< getContents
main = do { putStrLn . showTags . translate . parseTags $ testdata1
; putStrLn . showTags . translate . parseTags $ testdata2
; putStrLn . showTags . translate . parseTags $ testdata3
}
testdata1 = "<script foo=\"<script>alert('bar')</script>\">alert('foo')</script>"
testdata2 = "<script foo=\"<a href='link'>link</a>\">alert('foo')</script>"
testdata3 = "<a href='www.g>oogle.com'>link</a>"
{-
*Main> :main
<script foo="<script>alert('bar')</script>">alert('foo')</script>
<script foo="<a href='link'>link</a>">alert('foo')</script>
<a href='www.g>oogle.com'>link<a/>
-}
|
お題の更新に対応してみました。 一応「&」も「&」に置き換えるようにしました。 <BR>と<BR/>を受付け</BR>は置き換えるように変更しました。 ただし、サイズが少々でかすぎますね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | from HTMLParser import HTMLParser
from urllib import quote
class HTMLParser2(HTMLParser):
def reset(self):
HTMLParser.reset(self)
self.buf = ''
self.__endtag_text = ''
def parse_endtag(self, i):
try:
j = self.rawdata.index('>', i+1)
self.__endtag_text = self.rawdata[i:j+1]
except:
pass
return HTMLParser.parse_endtag(self, i)
def get_endtag_text(self):
return self.__endtag_text
def replace(self, s):
return s.replace('&', '&').replace('<', '<').replace('>', '>')
def handle_starttag(self, tag, attrs):
if tag == 'a':
self.buf += '<a '+' '.join(['%s="%s"' % (a, quote(b)) for a, b in attrs if a in ['href', 'name']])+'>'
elif tag in ['br', 'strong']:
self.buf += '<%s>' % tag
else:
self.buf += self.replace(self.get_starttag_text())
def handle_startendtag(self, tag, attrs):
if tag == 'br':
self.buf += '<br/>'
else:
self.buf += self.replace(self.get_starttag_text())
def handle_endtag(self, tag):
if tag in ['a', 'strong']:
self.buf += '</%s>' % tag
else:
self.buf += self.replace(self.get_endtag_text())
def handle_data(self, data):
self.buf += data
def f(s):
h = HTMLParser2()
h.feed(s)
print s
print h.buf
f('''<script foo="<script>alert('bar')</script>">alert('foo')</script>''')
f('''<script foo="<a href='link'>link</a>">alert('foo')</script>''')
f('''<a href='www.g>oogle.com'>link</a>''')
|
ぴったりなライブラリはどこかにあると思うのですが、 見付けられませんでした…。 とりあえず、みつけられたurl-rewiteと allegroのparse-htmlを使って作成してみました。 長くてぐちゃぐちゃですいません…。 嗚呼…。 ライブラリ: http://opensource.franz.com/xmlutils/xmlutils-dist/phtml.htm http://weitz.de/url-rewrite/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | #+allegro (progn (require :phtml) (use-package :net.html.parser))
(defun html-filter (string &optional in-prop-p)
(let ((form (if in-prop-p
(parse-html string)
(sanitize-html (parse-html string)))))
(apply #'concatenate 'string (build-html form in-prop-p))))
(defun sanitize-html (form)
(mapcar (lambda (item)
(if (atom item)
item
(let ((keys `(,(car item) ,(and (consp (car item)) (caar item)))))
(cond ((member :a keys)
`(,(car item) ,@(sanitize-html (cdr item))))
((member :strong keys)
`(:strong ,@(sanitize-html (cdr item))))
((member :br keys) :br)
('T item)))))
form))
(defun build-html (form &optional in-prop-p)
(if (atom form)
form
(cond ((and (atom (car form)) (eq :br (car form)))
`(,(br in-prop-p) ,@(build-html (cdr form) in-prop-p)))
((keywordp (car form)) (build-other form in-prop-p))
((consp (car form))
`(,(let* ((top (car form))
(keys `(,(car top) ,(and (consp (car top)) (caar top)))))
(if in-prop-p
(cond ((member-if #'keywordp keys) (build-other top in-prop-p))
('T top))
(cond (member :a keys) (build-a top))
((member :strong keys) (build-strong top))
((member-if #'keywordp keys) (build-other top in-prop-p))
('T top))))
,@(build-html (cdr form) in-prop-p)))
('T `(,(build-html (car form) in-prop-p)
,@(build-html (cdr form) in-prop-p))))))
(defun br (&optional in-prop-p)
(if in-prop-p "<br />" "<br />"))
(defun build-strong (form)
(format nil "<strong>~{~A~}</strong>" (build-html (cdr form))))
(defun build-a (form)
(let ((tag (car form))
(body (build-html (cdr form))))
(if (and (consp (car form))
(member (second tag) '(:href :name)))
(format nil "<a ~(~A~)='~A'>~{~A~}</a> "
(second tag)
(put-dot-slash-if-need (url-rewrite:url-encode (third tag))) body)
(format nil "<a>~{~A~}</a> " (build-html (cdr form))))))
(defun put-dot-slash-if-need (str)
(let ((s (cl-ppcre:create-scanner "^[Hh][Tt][Tt][Pp][Ss]*://")))
(if (cl-ppcre:scan s str)
str
(concatenate 'string "./" str))))
(defun build-other (form &optional in-prop-p)
(let ((tag (car form))
(body (build-html (cdr form))))
(if (consp tag)
(format nil "~(<~A~{ ~A~}~)>~{~A~}~0@*</~(~A~)> "
(car tag) (prop-maker (cdr tag)) body)
(if (and in-prop-p (eq :br tag))
"<br>"
(format nil "~(<~A>~)~{~A~}~0@*</~(~A~)>"
tag (build-html (cdr form)))))))
(defun prop-maker (lst)
(do ((l lst (cddr l))
result)
((endp l) (nreverse result))
(push (format nil "~A=\"~A\""
(car l) (html-filter (cadr l) t)) result)))
|
例によって正規表現が使えないので手続き的に。この調子だと、より複雑なことが要求される続編が思いやられます…(^_^;)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | | string in tag out save rest |
string := '<a href=''www.google.com''>link</a> <blink>and</blink> <strong onClick=''alert("NG")''>click<br/>me!</strong>'.
in := string readStream.
out := String new writeStream.
[in atEnd] whileFalse: [
out nextPutAll: (in upTo: $<).
in back.
save := in position.
tag := in upTo: Character space.
(tag includes: $/) ifTrue: [in position: save. tag := in upTo: $>. in back].
out nextPutAll: ((#('<a' '<br/' '<strong' '</a' '</strong') includes: tag asLowercase)
ifTrue: [tag] ifFalse: [tag := '<', tag allButFirst]).
tag := tag asLowercase.
[save := in position. (rest := in upTo: $>) includes: $=] whileTrue: [
| attr quote data |
in position: save.
attr := in upTo: $=.
quote := (#($' $") includes: in peek) ifTrue: [in next] ifFalse: [Character space].
data := in upTo: quote.
quote := quote = Character space ifTrue: [''] ifFalse: [quote asString].
data := attr, '=', quote, data, quote.
in skipSeparators.
(tag = '<a' and: [#(href name) includes: attr]) ifTrue: [out space; nextPutAll: data].
(#('<a' '<br/' '<strong') includes: tag) ifFalse: [
out space.
data do: [:chr | chr = $< ifTrue: [out nextPutAll: '<'] ifFalse: [out nextPut: chr]]]].
out nextPutAll: rest, '>'].
^out contents
"=> '<a href=''www.google.com''>link</a> <blink>and</blink> <strong>click<br/>me!</strong>' "
|
正規表現ないとこれはキツそうですね・・・ <br>がエスケープされるので<brをリストにいれたほうがいいと思います。あとattrがasLowercaseされていないような。HREFが削除されてしまいます。
HTMLParser,htmllibを使わずに、スクラッチから書いてみました。 フィルタの登録が簡単にできます。 ※ 字句解析部分は、十分にテストしてないので、全然 自信なしです。エンティティ等も未サポート・・・どころか誤動作起こす恐れもあり。 ※ フィルタの適用部分は、余計なメソッド呼び出しがたくさん発生する、冗長(無駄の多い)な実装です。 ※ 終了タグの扱いがad-hoc。(scan_htmlとpack_tag) ということなので、最初は汎用性とか考えて書いてた割りに、下の実装の品質は低いです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | import sys
import string
from cgi import escape
from urllib import quote
from itertools import imap
letters = string.letters + string.digits
TAG, ATTR, QUOTE, TEXT = range(4)
def scan_html(html):
tag = ''
text = ''
attr = ''
attrs = []
value = ''
state = TEXT
quoted = ''
closed = False
escaped = False
for c in html:
if state == TAG:
if c == ">":
yield TAG, tag, attrs, None
state = TEXT
elif c == '/' and tag == '':
tag += c
elif c in letters:
tag += c
elif c in string.whitespace:
state = ATTR
else:
pass
elif state == ATTR:
if c in letters:
attr += c
elif c in string.whitespace:
pass
elif c == '=' and not quoted:
state = QUOTE
elif c == '/':
tag += c
state = TAG
elif c == '>':
yield TAG, tag, attrs, None
state = TEXT
elif state == QUOTE:
if quoted:
if c == quoted:
if escaped:
escaped = False
value += c
else:
attrs.append((attr,value))
state = TAG
elif c == '\\':
escaped = True
value += c
else:
value += c
elif c in ('"', "'"):
quoted = c
else: # TEXT
if c == "<":
if text:
yield TEXT, tag, None, text
tag = ''
text = ''
attr = ''
attrs = []
state = TAG
else:
text += c
class MyHTMLParser:
def __init__(self):
self.tag_filters = {}
self.attr_filters = {}
self.text_filters = {}
self.allow_tags = []
self.forbid_tags = []
def filter(self, (state, tag, attrs, text)):
find_filter = lambda x:x.get((state,tag.lower()), lambda x:x)
tag,attrs = find_filter(self.tag_filters)((tag,attrs))
attrs = find_filter(self.attr_filters)(attrs)
text = find_filter(self.text_filters)(text)
return state, tag, attrs, text
def parse(self, html, output=sys.stdout.write):
def is_allowed_tag(tag):
if self.allow_tags and tag in self.allow_tags:
return True
if self.forbid_tags and not tag in self.forbid_tags:
return True
return False
def pack_tag(tag, attrs):
if tag.startswith("/") and not attrs:
return "<%s>" % tag
elif tag.endswith("/"):
if not attrs:
return "<%s />" % tag.strip('/ ')
return "<%s %s />" % (tag.strip('/ ')," ".join(map('%s="%s"'.__mod__,attrs)))
else:
if not attrs:
return "<%s>" % tag.strip('/ ')
return "<%s %s>" % (tag," ".join(map('%s="%s"'.__mod__,attrs)))
for event in imap(self.filter, scan_html(html)):
state,tag,attrs,text = event
if state == TAG:
if is_allowed_tag(tag.lower().strip(' /')):
output(pack_tag(tag,attrs))
else:
output(escape(pack_tag(tag,attrs)))
elif state == TEXT:
output(text)
def test(html):
def allow_attrs(*names):
return lambda attrs: [(k,quote(v)) for k,v in attrs if k.lower() in names]
def remove_all_attrs(attrs):
return []
p = MyHTMLParser()
p.allow_tags += ['a', 'br', 'strong']
p.attr_filters[(TAG,'a')] = allow_attrs('href', 'name')
p.attr_filters[(TAG,'br')] = remove_all_attrs
p.attr_filters[(TAG,'strong')] = remove_all_attrs
p.parse(html)
print
test('''<script foo="<script>alert('bar')</script>">alert('foo')</script>''')
test('''<script foo="<a href='link'>link</a>">alert('foo')</script>''')
test('''<a href='www.g>oogle.com'>link</a>''')
test('''<br />''')
test('''<img src="foo.jpg" />''')
test("""<a href='www.google.com'>link</a> <blink>and</blink> <strong onClick='alert("NG")'>click<br/>me!</strong>""")
test("""<strong style="color:red;">ok</strong>""")
test("""<br /><a href='localhost'>link</a><strong onClick='alert("NG")'>ok</strong>""")
|
まあ普通こういう処理はawkではしませんが・・・
% awk -f filter.awk
<script foo="<script>alert('bar')</script>">alert('foo')</script>
<script foo="<script>alert('bar')</script>">alert('foo')</script>
<script foo="<a href='link'>link</a>">alert('foo')</script>
<script foo="<a href='link'>link</a>">alert('foo')</script>
<a href='www.g>oogle.com'>link</a>
<a href="./www.g%3Eoogle.com">link</a>
なお、タグの途中に改行が含まれるケースには対応していません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | BEGIN {
ok_tag["A"] = ok_tag["BR"] = ok_tag["STRONG"] = 1
}
/<[A-Za-z]+/ {
delete attributes
s = $0
while (match(s,/<[A-Za-z]+/)) {
if (RSTART > 1) print substr(s,1,RSTART-1)
tag = substr(s,RSTART+1,RLENGTH-1)
s = substr(s,RSTART+RLENGTH)
if (s ~ /^>/) {
s = substr(s,2)
} else {
while (s != "") {
gsub(/^[ \t\r\n]*/,"",s)
if (match(s,/^[A-Za-z]+=/)) {
attribute = substr(s,1,RLENGTH-1)
s = substr(s,RLENGTH+1)
if (match(s,/^("[^"]*"|'[^']*'|[^"'> ]*)/)) {
value = substr(s,1,RLENGTH)
attributes[attribute] = value
s = substr(s,RLENGTH+1)
}
} else if (s ~ /^>/) {
s = substr(s,2)
break
} else {
break
}
}
}
tag_ = toupper(tag)
if (ok_tag[tag_]) {
if (tag_ == "A") {
for (attr in attributes)
if (toupper(attr) !~ /^(HREF|NAME)$/) delete attributes[attr]
} else { #if (tag ~ /^(BR|STRONG)$/)
delete attributes
}
printf("<%s", tag)
for (attr in attributes) {
value = urlencode(attributes[attr])
gsub(/(^["']|["']$)/,"",value)
if (toupper(attr) == "HREF") {
printf(" %s=\"./%s\"", attr, value)
} else {
printf(" %s=\"%s\"", attr, value)
}
}
printf(">")
} else {
printf("<%s", tag) # substr(s,RSTART+RLENGTH)
for (attr in attributes) {
printf(" %s=%s", attr, escape(attributes[attr]))
}
printf(">")
}
}
if (match(s,/<\/[A-Za-z]+>/)) {
content = substr(s,1,RSTART-1)
close_tag = substr(s,RSTART+2,RLENGTH-3)
s = substr(s,RSTART+RLENGTH)
if (content != "") printf("%s", escape(content))
tag_ = toupper(close_tag)
if (ok_tag[tag_]) {
printf("</%s>", close_tag)
} else {
printf("</%s>", close_tag)
}
} else if (s != "") {
printf("%s", s)
}
printf "\n"
next
}
{ print }
function escape(s)
{
gsub(/</,"\\<",s)
gsub(/>/,"\\>",s)
return s
}
function urlencode(s)
{
# gsub(/ /,"%20",s)
gsub(/</,"%3C",s)
# gsub(/=/,"%3D",s)
gsub(/>/,"%3E",s)
return s
}
|
適用しました。
すいません、このコードの34行に開き括弧が一つ足りませんでした。 (cond ((mem が正しいものです。
方針
間違っても、未処理のタグが残らないようにする(想定外であっても)。
想定外の構造のタグが誤って変換されてしまう事はやむを得ないとする。
この条件を満たすため、以下の方法で処理する
変換対象のタグを前処理する
全ての <, > を変換する
前処理したタグを元に戻す
&は題意から変換しないことにしました。尚、HTMLではURL中であっても文字参照は有効(ブラウザが解釈しなければならない)ため特別扱いはしていません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | import java.util.regex.*;
public class Sample {
private static final Pattern TAG_FILTER = Pattern.compile
("<(¥¥w+)((¥¥s+¥¥w+(¥¥s*=¥¥s*(¥"[^¥"]*¥"|'[^']*'|[¥¥w-:]*))?)*)¥¥s*/?¥¥s*>");
private static final Pattern END_TAG_FILTER = Pattern.compile
("(?i)</(A|BR|STRONG)¥¥s*>");
private static final Pattern ATTR_FILTER = Pattern.compile
("(¥¥w+)¥¥s*=¥¥s*(¥"[^¥"]*¥"|'[^']*'|[¥¥w-:]*)");
public static String sanitizing(String fragment) {
fragment = fragment.replaceAll("[¥¥p{Cntrl}&&[^¥¥s]]", "");
Matcher m = TAG_FILTER.matcher(fragment);
StringBuffer sb = new StringBuffer();
while (m.find()) {
if ("A".equalsIgnoreCase(m.group(1))) {
String href = null, name = null;
Matcher m2 = ATTR_FILTER.matcher(m.group(2));
while (m2.find()) {
if ("href".equalsIgnoreCase(m2.group(1))) {
href = m2.group(2);
} else if ("name".equalsIgnoreCase(m2.group(1))) {
name = m2.group(2);
}
}
String tag = "¥001"+m.group(1) + ((href != null)?" href="+href
: "") + ((name != null)? " name="+name : "") + "¥002";
m.appendReplacement(sb, m.quoteReplacement(tag));
} else if ("BR".equalsIgnoreCase(m.group(1)) ||
"STRONG".equalsIgnoreCase(m.group(1))) {
m.appendReplacement(sb, "¥001" + m.group(1) + "¥002");
}
}
m.appendTail(sb);
m = END_TAG_FILTER.matcher(sb.toString());
fragment = m.replaceAll("¥001/$1¥002");
fragment = fragment.replaceAll("<", "<");
fragment = fragment.replaceAll(">", ">");
fragment = fragment.replaceAll("¥001", "<");
fragment = fragment.replaceAll("¥002", ">");
return fragment;
}
public static void main(String[] args) throws Exception {
System.out.println(sanitizing("<script><abc><def ghi=jkl>"));
System.out.println(sanitizing("<script foo=¥"<script>alert(¥'bar¥')</script>¥">alert(¥'foo¥')</script>"));
System.out.println(sanitizing("<script foo=¥"<a href=¥'link¥'>link</a>¥" center>alert(¥'foo¥')</script><BR/>"));
System.out.println(sanitizing("<a href='www.g>oogle.com' id=125>link</a>"));
}
}
|
ocamllex で。 こんなに長くなるとは思わなかった。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | {
type attributes = (string * string) list (* name, value *)
type input =
| String of string
| SingleTag of string * attributes * int * int
| OpenTag of string * attributes * int * int
| CloseTag of string * int * int
| Eof
}
let tag_start = "<"
let close_tag_start = "</"
let tag_end = ">"
let single_tag_end = "/>"
let tag_constituent = ['a' - 'z' 'A' - 'Z']
let attr_constituent = ['a' - 'z' 'A' - 'Z' '-']
let white = [' ' '\t' '\n' '\r']
let quote_omittable = ['a' - 'z' 'A' - 'Z' '0' - '9' '.' '-']
rule main = parse
| tag_start { tag (Lexing.lexeme_start lexbuf) lexbuf }
| close_tag_start { close_tag (Lexing.lexeme_start lexbuf) lexbuf }
| [^'<']+ as s { String s }
| eof { Eof }
and tag p = parse
| tag_constituent+ as name { attr_list p name [] lexbuf }
| (_#tag_constituent)+ as s { String ("<"^s) }
and attr_list p tagname attrs = parse
| white+ { attr_list p tagname attrs lexbuf }
| (attr_constituent+ as name) white* '=' {
let value = attr_value lexbuf in
attr_list p tagname ((name, value)::attrs) lexbuf
}
| tag_end {
let endpos = Lexing.lexeme_end lexbuf in
OpenTag(tagname, List.rev attrs, p, endpos)
}
| single_tag_end {
let endpos = Lexing.lexeme_end lexbuf in
SingleTag(tagname, List.rev attrs, p, endpos)
}
| _ { attr_list p tagname attrs lexbuf }
and attr_value = parse
| white+ { attr_value lexbuf }
| '\'' ([^'\'']* as value) '\'' { value }
| '"' ([^'"']* as value) '"' { value }
| (quote_omittable* as value) { value }
| _ { attr_value lexbuf }
and close_tag p = parse
| (tag_constituent+ as name) [^'>']* tag_end {
CloseTag (name, p, Lexing.lexeme_start lexbuf)
}
| (_#tag_constituent)+ as text {
String ("</"^text)
}
{
let parse_input s =
let lexbuf = Lexing.from_string s in
let rec loop inputs =
match main lexbuf with
| Eof -> List.rev inputs
| x -> loop (x::inputs)
in loop []
let (===) s1 s2 = String.uppercase s1 = String.uppercase s2
let is_allowed_tag tagname =
List.exists (fun s -> tagname === s) ["a"; "br"; "strong"]
let is_allowed_attribute tagname attrname =
(* めんどくさくなったので ad-hoc *)
tagname === "a" && (attrname === "href" || attrname === "name")
let sanitizing_output buf string p q =
for x = p to q-1 do
match string.[x] with
| '<' -> Buffer.add_string buf "<"
| '>' -> Buffer.add_string buf ">"
| '&' -> Buffer.add_string buf "&"
| c -> Buffer.add_char buf c
done
let output_attribute_if_allowed buf tagname (attrname, value) =
if is_allowed_attribute tagname attrname then
let quote = if String.contains value '"' then '\'' else '"' in
Printf.bprintf buf " %s=%c%s%c" attrname quote value quote
let output_tag buf name attrs is_single =
Printf.bprintf buf "<%s" name;
List.iter (output_attribute_if_allowed buf name) attrs;
if is_single then Buffer.add_char buf '/';
Buffer.add_char buf '>'
let output_text_fragment buf src = function
| String s ->
sanitizing_output buf s 0 (String.length s)
| SingleTag (s, attrs, p, q) ->
if is_allowed_tag s then
output_tag buf s attrs true
else
sanitizing_output buf src p q
| OpenTag (s, attrs, p, q) ->
if is_allowed_tag s then
output_tag buf s attrs false
else
sanitizing_output buf src p q
| CloseTag (s, p, q) ->
if is_allowed_tag s then
Printf.bprintf buf "</%s>" s
else
sanitizing_output buf src p q
| Eof -> ()
let filter_text text =
let list = parse_input text in
let buf = Buffer.create (String.length text) in
List.iter (output_text_fragment buf text) list;
Buffer.contents buf
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | | string accepts in out upToAnyOf letters separators |
string := '<a title="(>_<;)" href=''www.google.com'' name=''hoge'' target=_blank>link</a> <blink>and</blink> <strong onClick=''alert("NG")''>click<br/>me!</strong>'.
accepts := {#a->#(name href). #strong->#(). #br->#()} as: Dictionary.
string := string copyReplaceAll: '<br>' with: '<br/>'.
in := string readStream.
out := String new writeStream.
upToAnyOf := [:arr | String streamContents: [:ss |
arr := arr copyWith: nil.
[arr includes: in peek] whileFalse: [ss nextPut: in next]]].
letters := Character alphabet asArray, Character alphabet asUppercase.
separators := Character separators, #($/ $>).
[out nextPutAll: (in upTo: $<) escapeEntities. in atEnd] whileFalse: [
| tag lt isClose isAccepted blank rest |
(isClose := in peek == $/) ifTrue: [in next].
tag := upToAnyOf value: separators.
lt := '<', (isClose ifTrue: ['/'] ifFalse: ['']).
(isAccepted := accepts keys includes: tag asLowercase) ifFalse: [lt := lt escapeEntities].
out nextPutAll: lt, tag.
[blank := upToAnyOf value: letters, '>'. {nil. $>} includes: in peek] whileFalse: [
| attr equal value quote |
attr := upToAnyOf value: #($= $>).
equal := in peek == $= ifTrue: [in next asString] ifFalse: [''].
value := (#($' $") includes: (quote := in peek))
ifTrue: [quote asString, (in next; upTo: quote), quote asString]
ifFalse: [upToAnyOf value: #($ $>)].
out nextPutAll: (isAccepted
ifFalse: [blank, attr, equal, value escapeEntities]
ifTrue: [((accepts at: tag) includes: attr)
ifTrue: [blank, attr, equal, value] ifFalse: ['']])].
rest := blank, (in peek == $> ifTrue: [in next asString] ifFalse: ['']).
out nextPutAll: (isAccepted ifTrue: [rest] ifFalse: [rest escapeEntities])].
World findATranscript: nil.
Transcript cr; show: out contents
"=> <a href='www.google.com' name='hoge'>link</a> <blink>and</blink> <strong>click<br/>me!</strong> "
|
HTMLの属性値の変換は、< > & " のHTML escapeだけを行なってます。
(html-filter
"<a href=\"www.google.com\">link</a> <blink>and</blink>\n<strong onClick='alert(\"NG\")'>click<br>me!</strong>")
=> "<a href=\"www.google.com\">link</a><blink>and</blink>\n<strong>click<br/>me!</strong>"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | (asdf:oos 'asdf:load-op :cl-html-parse)
(defconstant *enable-tags* '(("a" "href" "name")
("br")
("strong")))
(defun html-filter (html)
(html-element-filter (html-parse:parse-html html)))
(defun html-element-filter (elements)
(cond
((stringp elements)
(convert-not-tag elements))
((symbolp elements)
(convert-tag elements nil))
(t
(let* ((element-type (cond ((and (listp (car elements))
(symbolp (caar elements))
(not (null (cadar elements)))
(symbolp (cadar elements)))
:tag-attr)
((symbolp (car elements)) :tag)
(t :not-tag)))
(tag (case element-type
(:tag-attr (caar elements))
(:tag (car elements))))
(attr-lst (if (equal element-type :tag-attr)
(cdar elements)))
(elems (case element-type
(:tag-attr (cdr elements))
(:tag (cdr elements))
(:not-tag elements))))
(case element-type
(:tag-attr (convert-tag-attr tag attr-lst elems))
(:tag (convert-tag tag elems))
(:not-tag (string-join (mapcar #'html-element-filter elems))))))))
(defun convert-tag (tag elems)
(let* ((enable-tag (assoc (string-downcase tag) *enable-tags* :test #'equal))
(< (if enable-tag "<" "<"))
(> (if enable-tag ">" ">")))
(cond ((null elems)
(format nil "~a~(~a~)/~a" < tag >))
(t
(string-join
(format nil "~a~(~a~)~a" < tag >)
(html-element-filter elems)
(format nil "~a/~(~a~)~a" < tag >))))))
(defun convert-tag-attr (tag attr-lst elems)
(let* ((enable-tag (assoc (string-downcase tag) *enable-tags* :test #'equal))
(< (if enable-tag "<" "<"))
(> (if enable-tag ">" ">"))
(attr (loop for e in attr-lst by #'cddr
when (or (null enable-tag)
(member (string-downcase e) (cdr enable-tag)
:test #'equal))
collect (format nil "~(~a~)=\"~a\""
e
(escape-html (getf attr-lst e))))))
(cond
(elems
(string-join
(if attr
(format nil "~a~(~a~) ~{~a~^, ~}~a" < tag attr >)
(format nil "~a~(~a~)~a" < tag >))
(html-element-filter elems)
(format nil "~a/~(~a~)~a" < tag >)))
(t
(if attr
(format nil "~a~(~a~) ~{~a~^, ~}/~a" < tag attr >)
(format nil "~a~(~a~)/~a" < tag >))))))
(defun convert-not-tag (element)
(format nil "~a" (escape-html element)))
(defun string-join (&rest lst)
(let ((l (if (listp (car lst))
(car lst)
lst)))
(apply #'concatenate (cons 'string l))))
(defun escape-html (str)
(let ((in (make-string-input-stream str))
(out (make-string-output-stream)))
(loop for c = (read-char in nil nil)
while c
do (case c
(#\< (write-string "<" out))
(#\> (write-string ">" out))
(#\& (write-string "&" out))
(#\" (write-string """ out))
(t (write-char c out)))
finally (return (get-output-stream-string out)))))
|
この課題、非常に勉強になります。閉じタグ忘れに対応。 他の方ので、"<!--" がエスケープされないのがいくつか上がってる気がします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | using System;
using System.Text;
using System.Text.RegularExpressions;
class HtmlFilter {
private Regex _regFilter;
public HtmlFilter() {
_regFilter = new Regex(@"(<(?<begin>\w+)(?:\s+(?<attr>\w+\s*=\s*"
+ "(?:'[^']*'|\"[^\"]*\")" + @"\s*)*)?/?>)|(</(?<end>\w+)\s*>)",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
}
public string escape(string s) {
return s.Replace("&", "&").Replace("<", "<").Replace(">", ">");
}
public string filter(string input) {
bool tagA = false;
int index = 0, tagStrong = 0;
StringBuilder sb = new StringBuilder(input.Length * 2);
for (Match m = _regFilter.Match(input); m.Success; m = _regFilter.Match(input, index)) {
sb.Append(escape(input.Substring(index, m.Index - index)));
index = m.Index + m.Length;
if (m.Groups["begin"].Success) {
switch (m.Groups["begin"].Value.ToLower()) {
case "a":
string href = "";
foreach (Capture capture in m.Groups["attr"].Captures) {
string[] attribute = capture.Value.Split('=');
if (attribute[0].Trim().Equals("href", StringComparison.OrdinalIgnoreCase)) {
href = " href=" + attribute[1].Trim();
break;
}
}
sb.Append(tagA ? "</a><a" : "<a").Append(href).Append(">");
tagA = true;
break;
case "strong":
tagStrong++;
sb.Append("<strong>");
break;
case "br":
sb.Append("<br/>");
break;
default:
sb.Append(escape(m.Value));
break;
}
} else {
switch (m.Groups["end"].Value.ToLower()) {
case "a":
if (tagA) {
sb.Append("</a>");
tagA = false;
} else {
sb.Append(escape(m.Value));
}
break;
case "strong":
if (tagStrong > 0) {
tagStrong--;
sb.Append("</strong>");
} else {
sb.Append(escape(m.Value));
}
break;
default:
sb.Append(escape(m.Value));
break;
}
}
}
sb.Append(escape(input.Substring(index, input.Length - index)));
while (tagStrong-- > 0) sb.Append("</strong>");
if (tagA) sb.Append("</a>");
return sb.ToString();
}
}
class Program {
static void Main(string[] args) {
HtmlFilter f = new HtmlFilter();
string text = System.IO.File.ReadAllText("hoge.txt");
Console.WriteLine(f.filter(text));
}
}
|
ごり押しですが、strncasecmp に救われた気がします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | #include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUFSIZE 1024
char in[BUFSIZE];
int main( void ){
char *p, *tmp;
char q, attr=0, s='<';
int len=0, remain;
while( (remain = fgets( in + len, BUFSIZE - len, stdin )) || *p ) {
for(p=in;*p;p++){
if( remain && (len = strlen( p )) < strlen("<string") ){
tmp = (char*)malloc( sizeof(char) * len );
strcpy( tmp, p );
strcpy( in, tmp );
free(tmp);
break;
}
if( s == '<' ){
if( *p == '>' ){ printf(">"); }
else if( *p == '\'' || *p == '"' ){ putchar(s=*p); }
else if( *p != '<' ){ putchar(*p); }
else if( !strncasecmp( p, "</a>", 4) ){ printf("</a>"), attr=0; p+=3; }
else if( !strncasecmp( p, "<br", 3 ) ){
if( *(p+3) == '>' ){ printf("<br>"); p+=3; }
else if( *(p+3) == ' ' ){ printf("<br"); p+=3; s='>'; }
}
else if( !strncasecmp( p, "<strong", 7 ) ){
if( *(p+7) == '>' ){ printf("<strong>"); p+=7; }
else if( *(p+7) == ' ' ){ printf("<strong"); p+=7; s='>'; }
}
else if( !strncasecmp( p, "<a", 2 ) ){
if( *(p+2) == '>' ){ printf("<a>"); p+=2; }
else if( *(p+2) == ' ' ){ printf("<a"); p+=2; s='@'; }
}
else{ printf("<"); }
}
else if( s == '>' && *p == '>' ){ putchar(*p); s='<'; }
else if( s == '@' ){
q = *(p+5);
if( *p == '>' ){ putchar(*p); s='<'; }
else if( !strncasecmp( p, "href=", 5 ) && (q == '\'' || q == '"') ){
printf(" href=%c", s=q ); p+=6; attr='h';
}
else if( !strncasecmp( p, "name=", 5 ) && (q == '\'' || q == '"') ){
printf(" name=%c", s=q ); p+=6; attr='n';
}
}
else if( s == '"' || s == '\'' ){
if( !attr ){
if (*p == '<' ){ printf("<"); }
else if( *p == '>' ){ printf(">"); }
else{ putchar(*p); }
}
else{
if( *p == s ){ putchar(s); s='@'; }
else if( *p == '\\' && *(p+1) == s ){ printf("\\%c",s), p+=1; }
else if( attr=='h' && (tmp = strchr("!#$%'=|^\\[]`{}+<>", *p )) ){ printf("%%%X", *tmp); }
else{ putchar(*p); }
}
}
}
}
return 0;
}
|
HTML::Parserで割ときっちりと。 Dan the Perl Monger
see: HTML::Parser
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | #!/usr/local/bin/perl
use strict;
use warnings;
use HTML::Parser;
use HTML::Entities;
sub parse {
my $str = shift;
my @parsed;
my $p = HTML::Parser->new(
start_h => [
sub {
my ( $t, $a, $text ) = @_;
if ($t eq 'a'){
my @a =
map { qq/$_="/ . encode_entities($a->{$_}) . qq/"/ }
grep /^name|href$/, keys %$a;
push @parsed, "<a ".join(" ", @a). ">"
}elsif($t eq 'br' || $t eq 'strong'){
push @parsed, "<$t>"
}else{
push @parsed, encode_entities($text)
}
}, "tagname, attr, text"
],
end_h => [
sub {
my $t = shift;
push @parsed, $t =~ /^a|br|strong$/ ? "</$t>" : "<'/$t>";
}, "tagname"
],
text_h => [
sub {
my $t = shift;
$t =~ s{\n}{</br>}g;
push @parsed, $t;
}, "text"
]
)->parse($str);
join '', @parsed;
}
# for test
local $/;
my $str = <>;
print parse($str);
|
短かさにこだわってみました. < とかは反則ですか?
1 2 3 4 5 6 7 8 | #! /usr/bin/perl
sub E($){ local $_ = shift; s/([&<>"'])/sprintf('&#x%02X;',ord $1)/eg; $_; }
sub T($){ local $_ = shift; no warnings;
s#^<(/?(?:br|strong))\b.*$#<$1>#i ? $_ :
s#^<(/?a\b)[^>]*?(\s?(?:href|name)=(?:'[^']*'|"[^"]*"|[^>\s]*))?
[^>]*?(\s?(?:href|name)=(?:'[^']*'|"[^"]*"|[^>\s]*))?[^>]*?>
#<$1$2$3>#xi ? $_ : E $_; }
$_ = join '', <>; s#(.*?)(</?\w+\b[^>]*>)#E($1).T($2)#eg; print;
|
Gauche で書いてみました。まず、 HTML を HtmlPrag を使って SXML 形式に変換し、その上で属性のフィルタリングをします。タグの < や > を <、 > に変換するのは正規表現を使っています。ひとつのタグが複数行に分けて書かれている場合には対応していません。
see: HtmlPrag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | (load "htmlprag")
(use util.list)
(use sxml.serializer)
(use sxml.sxpath)
(use sxml.tools)
(use sxml.tree-trans)
(define (filter-html shtml)
(pre-post-order shtml
`((*default* . ,(lambda c c))
(*text* . ,(lambda (_ c) c))
(br . ,(lambda br
(sxml:change-attrlist! br '())
br))
(strong . ,(lambda strong
(sxml:change-attrlist! strong '())
strong))
(a . ,(lambda a
(sxml:change-attrlist! a
(cond-list
(((if-car-sxpath "./@href") a) => values)
(((if-car-sxpath "./@name") a) => values)))
a)))))
(define (main args)
(display
(call-with-input-file (cadr args)
(lambda (iport)
(regexp-replace-all
#/<((?!\/?a\b|\/?br\b|\/?strong\b|!--)[^>]*(?!--))>/
(srl:sxml->xml (filter-html (html->sxml iport)))
"<\\1>")))))
|
たぶんこれでできているのではないかな?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | #!/usr/bin/env groovy
String.metaClass.define{
escapeHtml{
delegate.replace("<", "<").replace(">", ">")
}
}
def text = System.in.text
// 属性値の置換
text = text.replaceAll(/(?<="|').+?[^\\](?='|")/){
it.escapeHtml()
}
// HTMLタグの置換
text = text.replaceAll(/<\/?(\w+).*?\/?>/){ all, name ->
def ret = ""
switch( name ){
case ~/(?i)a/:
// aタグhref属性のみURLエンコード処理
ret = all.replaceAll(/(?<=href="|').+?[^\\](?='|")/){
it.replace("<","%3C").replace(">", "%3E")
}
break
case ~/(?i)br|strong/:
ret = "<${name} />"
break
default:
ret = all.escapeHtml()
break
}
ret
}
println text
|






にしお
#3410()
Rating1/1=1.00
このお題はperezvonさんの提案を元にしています。ありがとうございました。 ただ、いきなりだと難しいかと思ったので、肝の部分以外を先に出題しました。このお題は続編で徐々に難しくなっていきます。
追記:属性に<や>が含まれてしまうケースに漏れのある解答が多いようなのでテストケースを追加します。 これは「この出力なら十分」という意味です。この出力の通りでなければいけないという意味ではありません。 <script foo="<script>alert('bar')</script>">alert('foo')</script> <script foo="<script>alert('bar')</script>">alert('foo')</script> <script foo="<a href='link'>link</a>">alert('foo')</script> <script foo="<a href='link'>link</a>">alert('foo')</script> <a href='www.g>oogle.com'>link</a> <a href="./www.g%3Eoogle.com">link</a>[ reply ]