Node.js 用に昔作ってたんだけど、ソースを読んだらスパゲッティソースで放置してた。
WebMessage によるクロスドメイン間のネタを投下したばかりで、これも公開すべきかなと思ったので。
WebMessage のネタ。気づいていたけど書かなかった文言が、「バックドア技術でもある」ということ。
WebMessage を「安全に」使うなら、CORS技術とCSP技術を修得して併用する「べき」なんですよねぇ。
WebMessage のコードを仕込まれたときに変な挙動を起こさないようにする防護策としても必須になってくると思う。CORS技術やCSP技術を一切使ってないサイトは攻撃対象として踏み台にされる可能性が高まる。
キーロガー型のユーザ関数を受理・実行されるようにもなったら、フォームに入力を促すアカウント情報が抜かれることも想定される。意図しないオリジンからの WebMessage を受け付けないようにする為に、CSP技術を使ってホワイトリストを明確化しておくのがベターです。
JavaScriptの有効化を前提としたサイトはブラウザ側の自己防衛機能を有効化させるために、両技術を使うべき時代に突入し、素人が趣味で…という10年前とは環境も大きく変わってますよね。
さて、モダンブラウザでは整備されてるけど、イマイチとっつきにくくて使ってるサイトも簡単には見つかない、CORS技術やCSP技術。このエントリは後者の CSP について。
で、Content Secure Policy (バージョン 1.0)についてのライブラリ。
(function(global, undefined){
"use strict";
var RE_SINGLEQUOTED, RE_CSP_KEYWORDS, RE_CSP_DIRECTIVE, RE_CSP_HOST, RE_CSP_HOST_EXPR;
RE_SINGLEQUOTED = /^(\')([^\1]+)\1$/;
RE_CSP_KEYWORDS = /^(none|self|(?:unsafe\-)?inline|(?:unsafe\-)?eval)$/
RE_CSP_DIRECTIVE = /.*(\-(?:src|uri))?$/i;
RE_CSP_HOST_EXPR = /((?:https?\:\/\/)?(?:(?:\*|[\d\w\-]+)(?:\.(?:\*|[\d\w\-]+))*)(?:\:(?:\*|\d+))?)/i;
function hasAnyValue(v){return Boolean(v)};
function trim(s){return s.trim()};
function validHostExpr(expr){return RE_CSP_HOST_EXPR.test(expr)};
function CSPSourceExpression(targetDirective){
this.directive = targetDirective;
this.schemeSources = [];
this.hostSources = [];
this.keywordSources = [];
};
CSPSourceExpression.prototype.setHost = function(hostExpr){
if(!hostExpr) {
this.hostSources.length = 0;
return;
}
if(hostExpr === '*'){
this.hostSources = ["*"];
return;
}
if( !validHostExpr(hostExpr) ) return;
if(this.hostSources[1] === '*') this.hostSources.length = 0;
this.hostSources.push(hostExpr);
return true;
};
CSPSourceExpression.prototype.getHostExpr = function(){
if(this.hostSources.length) return this.hostSources.filter(hasAnyValue).join(' ');
};
CSPSourceExpression.prototype.setScheme = function(scheme){
if(!scheme) {
this.schemeSources.length=0;
return;
}
// Version "1.0" では data スキームのみ。
if(!/data\:?$/.test(scheme)) return;
if(scheme.substr(-1) !== ':') scheme+=':';
this.schemeSources.push(scheme);
return true;
};
CSPSourceExpression.prototype.getScheme = function(){
if(this.schemeSources.length) return this.schemeSources.filter(hasAnyValue).join(' ');
};
CSPSourceExpression.prototype.setKeyword = function(key){
var m, k, v, u;
if(!(k=key)) {
this.keywordSources.length = 0;
return;
}
if(m = RE_SINGLEQUOTED.exec(k)){ v = m[0]; k = m[2]; }
if(!RE_CSP_KEYWORDS.test(k)) return;
if(/^(inline|eval)$/.test(k)) k = 'unsafe-' + k;
v = "'" + k + "'";
// unsafe-keyword is default-src, script-src, style-src
if(!/^(default|script|style)/.test(this.directive) && /^unsafe/.test(k) ){
return;
}
u = "'none'";
if(k==='none') {
this.keywordSources = [u];
}
else {
if(this.keywordSources[0] === u) this.keywordSources.length=0;
this.keywordSources.push(v);
}
return true;
};
CSPSourceExpression.prototype.getKeywords = function(){
if(this.keywordSources.length) return this.keywordSources.filter(hasAnyValue).join(' ');
};
CSPSourceExpression.prototype.setExpression = function( value ){
return this.setScheme( value ) || this.setKeyword( value ) || this.setHost(value);
};
CSPSourceExpression.prototype.getExpression = function(){
// check set policy
return (this.schemeSources.length || this.hostSources.length || this.keywordSources.length ) &&
([
this.directive,
this.getKeywords(),
this.getHostExpr(),
this.getScheme()
].filter(hasAnyValue).join(' '));
};
function CSP(ver){
this.version = ver || CSP.defaultVersion;
this.exprs = {};
};
CSP.defaultVersion = "1.0";
CSP.headers = {
"1.0": 'Content-Security-Policy'
};
CSP.directives = {
"1.0": {
default: 'default-src',
script: 'script-src',
object: 'object-src',
img: 'img-src',
media: 'media-src',
frame: 'frame-src',
font: 'font-src',
connect: 'connect-src',
style: 'style-src',
report: 'report-uri'
}
};
CSP.prototype.setPolicy = function(directive, value){
var directives;
if(!RE_CSP_DIRECTIVE.test(directive+'')) return;
directives = CSP.directives[this.version];
directive = directive.replace(RE_CSP_DIRECTIVE, RegExp.leftContext).toLowerCase();
if(directive === 'report'){
// isURL() 関数は未実装。
// return isURL(value) ? (this.exprs[directive]=value): void 0;
this.exprs[directive] || (this.exprs[directive]=value);
return;
}
this.exprs[directive] || (this.exprs[directive]=new CSPSourceExpression(directives[directive]));
if(!this.exprs[directive].setExpression(value)) {
console.log(' cant set csp-policy ');
}
};
CSP.prototype.getHeaderField = function(){
return CSP.headers[this.version] || CSP.headers[CSP.defaultVersion];
};
CSP.prototype.getPolicyToken =
CSP.prototype.getHeaderValue = function(){
var directives;
directives = CSP.directives[this.version];
return Object.keys(directives).map(function(key){
if(!this.exprs[key]) return;
return key === 'report'
? directives[key] + ':' +this.exprs[key]
: this.exprs[key].getExpression();
}, this).filter(hasAnyValue).join('; ')
}
// factory & utilities
global['CSP'] = function(ver){
return new CSP(ver);
};
// サーバー側でJSONから一括設定するための実装
global['CSP'].fromJSON = function(obj){
var csp;
obj || (obj={})
csp = new CSP(obj.version);
Object.keys(obj).forEach(function( directive ){
if(!RE_CSP_DIRECTIVE.test(directive+'')) return;
var setValues = obj[directive];
if(!Array.isArray(setValues)) setValues = [setValues];
setValues.forEach(function(val){
csp.setPolicy(directive, val);
});
});
return csp.getHeaderValue();
};
// クライアント側で XMLHttpRequest() のヘッダ取得時に解析するための実装
global['CSP'].toJSON = function( headerValue ){
var rslt;
(headerValue.split(/\s*\;\s*/)).forEach(function(source){
var vals, directive;
vals = source.split(/\s+/);
directive = trim(vals.shift()).replace(RE_CSP_DIRECTIVE, RegExp.leftContext).toLowerCase();
this[directive] = vals.map(trim);
}, rslt = {});
return rslt;
};
}(this, void 0));
CSP とは、サーバー応答時にヘッダフィールド Content-Security-Policy を含めることで、ブラウザの挙動を制限できる仕様。
バージョン 1.1 が検討される現在、Firefox なんかではバージョン 1.0 が普通に機能します。
で、ヘッダ「Content-Security-Policy」に与える値(ポリシー・トークン)は複雑で、コードに埋め込もうとするとTYPO も多いので、ライブラリで適切な ポリシートークンを自動生成しちゃうのが開発の目的でした。
ここで公開してるのは、AJAXでヘッダを受け取った時に解析しやすいよう、ポリシー・トークンをJSON化するユーティリティ関数も追加。
*-src とか *-uri とか、ディレクティブの正式名称がチトうざい。
キーワードはシングルクォートで囲う必要があるとか、スキームは最後に コロン記号(:)を必要とするとかもチトうざい。
なので、そういう面倒な部位文を端折って設定できる実装にしてます。
(1.1の仕様では端折りづらいディレクティブとして、frame-ancestor, frame-src があるので、拡張しづらいけど)
ちなみに、exports に代入していないのは、ブラウザの開発者コンソールで確認した段階のソース。もし使うという方は、適宜、サーバーに対応させてください。
あ、ライブリ化した理由は、「応答するリソースによって内容を変える」のが目的です。
取り敢えず、自分のドメイン以外のアクセス禁止したいだけなら、
apache なら .htaccess に書いておくとか スタティックに設定しちゃえばいい。
Header set X-Content-Security-Policy "default-src 'self'"
Header set X-WebKit-CSP "default-src 'self'"
Header set Content-Security-Policy "default-src 'self'"
サーバーアプリらしく動作させたいなら、リソース毎にインテリジェンスなCSP応答したい。