一部のHTMLタグを通すフィルタ
Posted feedbacks - Nested
Flatten Hiddenタグを無効化するために「<」のサニタイズするのなら 「;」も対象にしたほうが良い予感?
それはなぜですか?
エンティティの無効化です。 このお題で必要かと問われると怪しいですけど、 きっとタグが有効になると何か影響でちゃうのですよね? タグが特別なものであるとしてとらえると タグを無効化する必要のある処理であるならば エンティティも一緒に無効化しても良いかなと。 個人的な好みが強いですが片手落ちというイメージが。。。
なるほど。エンティティや実体参照を許すとどういう脆弱性につながるのかは寡聞にして知りませんが、セミコロンをエスケープしても要件を満たしていそうなので気になるのならそうしてもいいと思います。
ちなみに♥のセミコロンを&semicolon;に置き換えてもハートマークは表示されるみたいですね。
このサイトはセミコロンをエスケープしていませんが、アンパサンドはエスケープしています。
きゅ~勘違いしてました。 「;」のエンティティって存在しないみたいです。 「&」で十分です。 昔、エンティティを無効にするときは「;」というのを読んだ気がしたんだけど、うーんソースが見つからない。 なんでエンティティを無効にしないといけなかったんだっけな~? 何か理由があったはずなんだけど。
実体参照の話であれば ; というよりも & を & にしなくていいいのかということですよね。違うかな。。
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. プリンタ (構文木→文字列) と分けて パーザは 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/>
-}
|
ライブラリを使ったら面白くないので、自分で書いてみた。思ったよりすっきりかけた気がする。
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));
|
ちょっと無駄に難しく作りすぎました。 開きタグと閉じタグは個別に扱っても構わなかったのか。 ところで、(要件に含まれるのかわかりませんが)属性に">"が含まれるケースを考慮してない回答が結構ありますね。
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);
|
>属性に">"が含まれるケースを考慮してない回答が結構ありますね。
ちなみに僕はどのコードがダメなのか公表してしまった方が(そのコードを書いた人が将来その罠に気づかずに脆弱性のあるプログラムを書いてしまうよりは)いいと思っているので、その手の指摘はたいがいプラス評価しています。
バグありました… 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, '<');
}
|
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);
|
>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);
|
CGI.escapeElementが使えるかなと思ったが微妙に要求が異なる
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
|
タグに大文字小文字が混在している場合や、属性が大文字の場合が考慮されていないのでは?少なくともtagnameはdowncaseしてから比較したほうがよい気がします。
修正。
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
|
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> となってしまいます。
なるほど、そんな場合があるのか・・・というわけで修正版です。
属性をパースする正規表現が二度出てくるのが美しくないですが・・・このあたりをPythonでうまく処理する方法ってあるのかな。
属性をパースする正規表現が二度出てくるのが美しくないですが・・・このあたりを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()
|
oceanさんのコードで確かめたのでここにぶら下げますが、他のポストにも同じ問題があるかもしれません。
入力文字列が次のようなものだとまずいのでは:
<z foo='<script>alert("Boo")</script>'>
なるほど、タグを無効化した場合、属性に含まれる < を放置すると今度はそちらがタグとみなされるということですか。
というわけで再修正パッチです。でもきっとまだあるな・・・
というわけで再修正パッチです。でもきっとまだあるな・・・
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()
|
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 |




にしお
#3410()
Rating0/0=0.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 ]