Thin Hook Preprocessor
Thin Hook Preprocessor (experimental)
Object.prototype
, access policy objects are immune to contaminated Object.prototype
properties when targetConfig.policy.unchainAcl = true
is configured (which is disabled by default for compatibility). Prior to this version, writable Object.prototype
properties can contaminate access policy objects.Object.prototype
object properly, acl.Object[S_PROTOTYPE]
policies for Object.prototype
properties are properly applied when targetConfig.policy.unchainAcl = true
is configured (which is disabled by default for compatibility). Prior to this version, acl.Object
policies are incorrectly applied for Object.prototype
properties in some cases.hook.parameters.importMapper = null
in demo/bootstrap.js
<a download="filename" href="blob:...">Download Link</a>
. Prior to this version, documents with blob URLs bypass Service Worker.<embed>
and <object>
elements, the application hangs up on <embed>
and <object>
activities with hook.parameters.hangUpOnEmbedAndObjectElement = true
. Prior to this version, <embed>
and <object>
documents can bypass Service Worker with Chrome Canary 86.new Proxy(target, handler)
and Proxy.revocable(target, handler)
as with their original target
objects. Prior to this version, ACL for the target
objects are not applied to proxy objects.Policy.defaultAcl()
, ACL is properly applied for with
-scoped values in function calls and constructor calls. Prior to this version, calls to with
-scoped functions can skip ACLs for their reference values.with
clause, local function calls in with
clause are property hooked. Prior to this version, local function calls in with
clause are not hooked. This is a regression issue from the fix for Fix #339 Local variables in a with block are mistreated as global variables in ACL #339.acl.type[S_PROTOTYPE][S_INSTANCE]
ACLs for primitive type classes String
, Number
, Boolean
, Symbol
, and BigInt
are applied to properties of primitive values. Prior to this version, ACL is not applied to properties of primitive values.with
namespace objects before bound function detection, bound function calls in a with
clause is properly detected. Prior to this version, ACL for unbound original function is not applied for bound function calls in a with
clause.acl[S_DEFAULT][S_PROTOTYPE][S_INSTANCE]
to acl.Object[S_PROTOTYPE][S_INSTANCE]
, acl.Object[S_PROTOTYPE][S_INSTANCE]
is applied for anonymous object properties. Prior to this version, acl.Object[S_PROTOTYPE][S_INSTANCE]
is not applied anonymous object properties.Object.assign()
even they contain falsy values. Prior to this version, ACL is not applied for sources of Object.assign()
if the first source is not an object like undefined
.receiver
in Reflect.get()
, ACL is applied for receiver
in Reflect.get()
to read the target property. Prior to this version, ACL is not applied for receiver
in Reflect.get()
.Object.assign()
by checking their constructors. Prior to this version, ACL for class instances as source objects in Object.assign()
is not applied while ACL for global objects is applied properly. This fix is to supplement the fix for Fix #324 Apply ACL for S_TARGETED normalized properties with S_ALL normalized property.Object.defineProperty()
, etc. are properly tracked for ACL. Prior to this version, ACL is skipped for global objects set via Object.defineProperty()
, etc.Object.assign()
are checked against ACL for reading all properties. Prior to this version, ACL is skipped for source objects in Object.assign()
and any enumerable properties in the source objects can be assigned to the target object.about:blank
is responded for GET errorReport.json request. Prior to this version, 404 Not Found is responded for GET errorReport.json, whose HTML contents in iframe can be accessed bypassing access policies.cache-bundle.json
to add additional cacheable content types. This README document is updated to describe the new features and their configurations.hook.parameters.appPathRoot = '/';
in demo/disable-devtools.js
can be configured to set the root of the application assets. Prior to this version, direct access to source codes are allowed.blob:
and data:
URLs are blocked for SVG. Prior to this version, scripts in SVG are not hoooked and blob:
and data:
URLs are allowed for SVG. <object data="inline-script.svg"></object>
, <embed src="inline-script.svg">
, <iframe src="inline-script.svg"></iframe>
blob:
and data:
URLs are blocked for Worker
and SharedWorker
. Prior to this version, blob:
and data:
URLs are allowed for Worker
and SharedWorker
.iframe.srcdoc
is hooked as onload
attribute. Prior to this version, iframe.srcdoc
is not hooked.AsyncFunction('script')
is properly hooked. Prior to this version, AsyncFunction('script')
is not hooked. AsyncFunction = (async function() {}).constructor
object.Function('script')
is properly hooked. Prior to this version, object.Function('script')
is not hooked.__hook__acl
in demo/hook-callback.js
should be used as it is much faster than __hook__
as described in Fix #230. Modification: Object.defineProperty(_global, '__hook__', { configurable: false, enumerable: false, writable: false, value: hookCallbacks.__hook__acl });
top
, parent
, frames
, global
, _global
, etc.) other than the main global object property (window
in the main document, self
in workers) are applied only for access like window.top
. In 0.0.224, all the ACLs for the global object properties are applied for every global object access, which is redundant._globalObjects
is a SetMap
object defined in hook-callback.js
and _globalObjects.get(obj)
return a Set
object containing string
s. All the ACLs for the set of string
s are applied for the object. Prior to this version, _globalObjects
is a Map
object and _globalObjects.get(obj)
returns a string
._blacklistObjects
is deprecated.delete
operations require 'W'
permission as they can delete properties with customized descriptors. Prior to this version, delete
operations require 'w'
permission.'R'
and 'W'
opTypes are introduced for getting/setting property descriptors, i.e., contexts to access descriptors must have explicit 'R'
and/or 'W'
permissions for the target properties. Prior to 0.0.213, property descriptors can be accessed by mere 'r'
and/or 'w'
permissions.document.writeln()
are hooked as in document.write()
. Prior to this version, scripts via document.writeln()
are not hooked.textContent
of script
elements are always treated as JavaScript scripts regardless of their configured MIME types (type
property/attribute). Prior to this version, textContent
of script
elements containing __hook__
as strings can be mistaken as HOOKED scripts and run without hooking."method"
is renamed as "oldMethod"
and the "cachedMethod"
is renamed as "method"
and become the new default context generator. The "cachedMethod"
remains as an alias for the new "method"
context generator. There are slight changes in the new "method"
context generator. A warning message is shown on the debug console to notify the change.old name | new name | feature |
---|---|---|
method |
oldMethod |
script.js,Class,method |
cachedMethod |
method |
script.js,Class,method including computed property names |
hook.__hook__
. hook.hookCallbackCompatibilityTest()
can detect if the target hook callback function is compatible or not. hook.parameters.opaque = [ 'opaque_url', ..., (url) => url.match(/opaque_url_pattern/), ... ]
configuration.Demo on GitHub Pages
class C {
add(a = 1, b = 2) {
let plus = (x, y) => x + y;
return plus(a, b);
}
}
const __context_mapper__ = $hook$.$(__hook__, [
'examples/example2.js,C',
'_p_C;examples/example2.js,C',
'examples/example2.js,C,add',
'examples/example2.js,C,add,plus'
]);
$hook$.global(__hook__, __context_mapper__[0], 'C', 'class')[__context_mapper__[1]] = class C {
add(a, b) {
return __hook__((a = 1, b = 2) => {
let plus = (...args) => __hook__((x, y) => x + y, null, args, __context_mapper__[3]);
return __hook__(plus, null, [
a,
b
], __context_mapper__[2], 0);
}, null, arguments, __context_mapper__[2]);
}
};
const hook = require('thin-hook/hook.js');
let code = fs.readFileSync('src/target.js', 'UTF-8');
let initialContext = [['src/target.js', {}]];
let gen = hook(code, '__hook__', initialContext, 'hash');
fs.writeFileSync('hooked/target.js', gen);
fs.writeFileSync('hooked/target.js.contexts.json', JSON.stringify(contexts, null, 2));
// Built-in Context Generator Function
hook.contextGenerators.method = function generateMethodContext(astPath) {
return astPath.map(([ path, node ], index) => node && node.type
? (node.id && node.id.name ? node.id.name : (node.key && node.key.name
? (node.kind === 'get' || node.kind === 'set' ? node.kind + ' ' : node.static ? 'static ' : '') + node.key.name : ''))
: index === 0 ? path : '').filter(p => p).join(',');
}
// Example Custom Context Generator Function with Hashing
const hashSalt = '__hash_salt__';
let contexts = {};
hook.contextGenerators.hash = function generateHashContext(astPath) {
const hash = hook.utils.createHash('sha256');
let hashedInitialContext = astPath[0][0];
astPath[0][0] = contexts[hashedInitialContext] || astPath[0][0];
let methodContext = hook.contextGenerators.method(astPath);
astPath[0][0] = hashedInitialContext;
hash.update(hashSalt + methodContext);
let hashContext = hash.digest('hex');
contexts[hashContext] = methodContext;
return hashContext;
}
{
// Authorization Tickets for no-hook scripts
// Ticket for this script itself is specified in URL of script tag as
// hook.min.js?no-hook-authorization={ticket}
// Note: no-hook-authorization must not exist in learning mode
let noHookAuthorization = {
// '*' is for learning mode to detect authorization tickets in
// hook.parameters.noHookAuthorizationPassed,
// hook.parameters.noHookAuthorizationFailed
// JSONs are output to console in the learning mode
//'*': true,
"35ae97a3305b863af7eb0ac75c8679233a2a7550e4c3046507fc9ea182c03615": true,
"16afd3d5aa90cbd026eabcc4f09b1e4207a7042bc1e9be3b36d94415513683ed": true,
"ae11a06c0ddec9f5b75de82a40745d6d1f92aea1459e8680171c405a5497d1c8": true,
"5b7ebf7b0b2977d44f47ffa4b19907abbc443feb31c343a6cbbbb033c8deb01a": true,
"c714633723320be54f106de0c50933c0aeda8ac3fba7c41c97a815ed0e71594c": true,
"2f43d927664bdfcbcb2cc4e3743652c7eb070057efe7eaf43910426c6eae7e45": true,
"b397e7c81cca74075d2934070cbbe58f345d3c00ff0bc04dc30b5c67715a572f": true,
"02c107ea633ed697acc12e1b3de1bcf2f0ef7cafe4f048e29553c224656ecd7a": true,
"aebb23ce36eb6f7d597d37727b4e6ee5a57aafc564af2d65309a9597bfd86625": true
};
let hidden;
const passcode = 'XX02c107ea633ed697acc12e1b3de1bcf2f0ef7cafe4f048e29553c224656ecd7a';
if (typeof self === 'object' && self.constructor.name === 'ServiceWorkerGlobalScope') {
// Service Worker
let reconfigure = false;
if (hook.parameters.noHookAuthorization) {
if (Object.getOwnPropertyDescriptor(hook.parameters, 'noHookAuthorization').configurable) {
reconfigure = true;
}
}
else {
reconfigure = true;
}
if (reconfigure) {
Object.defineProperty(hook.parameters, 'noHookAuthorization', {
configurable: false,
enumerable: true,
get() {
return hidden;
},
set(value) {
if (value && value.passcode === passcode) {
delete value.passcode;
Object.freeze(value);
hidden = value;
}
}
});
}
noHookAuthorization.passcode = passcode;
hook.parameters.noHookAuthorization = noHookAuthorization;
}
else {
// Browser Document
Object.defineProperty(hook.parameters, 'noHookAuthorization', {
configurable: false,
enumerable: true,
writable: false,
value: Object.freeze(noHookAuthorization)
});
}
if (!noHookAuthorization['*']) {
Object.seal(hook.parameters.noHookAuthorizationPassed);
}
}
{
// source map target filters
hook.parameters.sourceMap = [
url => location.origin === url.origin && url.pathname.match(/^\/components\/thin-hook\/demo\//)
];
// hook worker script URL
hook.parameters.hookWorker = `hook-worker.js?no-hook=true`;
}
// Hook worker script (demo/hook-worker.js)
//
// Configuration:
// hook.parameters.hookWorker = `hook-worker.js?no-hook=true`;
importScripts('../hook.min.js?no-hook=true', 'context-generator.js?no-hook=true', 'bootstrap.js?no-hook=true');
onmessage = hook.hookWorkerHandler;
<!-- Example Custom Context Generator for Service Worker and Browser Document -->
<script src="bower_components/thin-hook/hook.min.js?version=1&no-hook=true&hook-name=__hook__&context-generator-name=method2&fallback-page=index-fb.html&service-worker-ready=true"></script>
<script context-generator src="custom-context-generator.js?no-hook=true"></script>
<script context-generator no-hook>
{
hook.contextGenerators.method2 = function generateMethodContext2(astPath) {
return astPath.map(([ path, node ], index) => node && node.type
? (node.id && node.id.name ? node.id.name : (node.key && node.key.name
? (node.kind === 'get' || node.kind === 'set' ? node.kind + ' ' : node.static ? 'static ' : '') + node.key.name : ''))
: index === 0 ? path : '').filter(p => p).join(',') +
(astPath[astPath.length - 1][1].range ? ':' + astPath[astPath.length - 1][1].range[0] + '-' + astPath[astPath.length - 1][1].range[1] : '');
}
Object.freeze(hook.contextGenerators);
// CORS script list
hook.parameters.cors = [
'https://raw.githubusercontent.com/t2ym/thin-hook/master/examples/example1.js',
(url) => { let _url = new URL(url); return _url.hostname !== location.hostname && ['www.gstatic.com'].indexOf(_url.hostname) < 0; }
];
// Authorized opaque URL list
hook.parameters.opaque = [
'https://www.gstatic.com/charts/loader.js',
(url) => {
let _url = new URL(url);
return _url.hostname !== location.hostname &&
_url.href.match(/^(https:\/\/www.gstatic.com|https:\/\/apis.google.com\/js\/api.js|https:\/\/apis.google.com\/_\/)/);
}
];
}
</script>
// Built-in Minimal Hook Callback Function without hooking properties (hook-property=false)
hook.__hook_except_properties__ = function __hook_except_properties__(f, thisArg, args, context, newTarget) {
return newTarget
? Reflect.construct(f, args)
: thisArg
? f.apply(thisArg, args)
: f(...args);
}
// the global object
const _global = (new Function('return this'))();
// helper for strict mode
class StrictModeWrapper {
static ['#.'](o, p) { return o[p]; }
static ['#[]'](o, p) { return o[p]; }
static ['#*'](o) { return o; }
static ['#in'](o, p) { return p in o; }
static ['#()'](o, p, a) { return o[p](...a); }
static ['#p++'](o, p) { return o[p]++; }
static ['#++p'](o, p) { return ++o[p]; }
static ['#p--'](o, p) { return o[p]--; }
static ['#--p'](o, p) { return --o[p]; }
static ['#delete'](o, p) { return delete o[p]; }
static ['#='](o, p, v) { return o[p] = v; }
static ['#+='](o, p, v) { return o[p] += v; }
static ['#-='](o, p, v) { return o[p] -= v; }
static ['#*='](o, p, v) { return o[p] *= v; }
static ['#/='](o, p, v) { return o[p] /= v; }
static ['#%='](o, p, v) { return o[p] %= v; }
static ['#**='](o, p, v) { return o[p] **= v; }
static ['#<<='](o, p, v) { return o[p] <<= v; }
static ['#>>='](o, p, v) { return o[p] >>= v; }
static ['#>>>='](o, p, v) { return o[p] >>>= v; }
static ['#&='](o, p, v) { return o[p] &= v; }
static ['#^='](o, p, v) { return o[p] ^= v; }
static ['#|='](o, p, v) { return o[p] |= v; }
static ['#.='](o, p) { return { set ['='](v) { o[p] = v; }, get ['=']() { return o[p]; } }; }
}
// Built-in Minimal Hook Callback Function with hooking properties (hook-property=true) - default
function __hook__(f, thisArg, args, context, newTarget) {
let normalizedThisArg = thisArg;
if (newTarget === false) { // resolve the scope in 'with' statement body
let varName = args[0];
let __with__ = thisArg;
let scope = _global;
let _scope;
let i;
for (i = 0; i < __with__.length; i++) {
_scope = __with__[i];
if (Reflect.has(_scope, varName)) {
if (_scope[Symbol.unscopables] && _scope[Symbol.unscopables][varName]) {
continue;
}
else {
scope = _scope;
break;
}
}
}
thisArg = normalizedThisArg = scope;
}
let result;
let args1 = args[1]; // for '()'
function * gen() {}
let GeneratorFunction = gen.constructor;
switch (f) {
case Function:
args = hook.FunctionArguments('__hook__', [[context, {}]], 'method', args);
break;
case GeneratorFunction:
args = hook.FunctionArguments('__hook__', [[context, {}]], 'method', args, true);
break;
case '()':
case '#()':
switch (thisArg) {
case Reflect:
switch (args[0]) {
case 'construct':
if (args[1]) {
switch (args[1][0]) {
case Function:
args1 = [args[1][0], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][1])];
if (args[1][2]) {
args1.push(args[1][2]);
}
break;
default:
if (args[1][0].prototype instanceof Function) {
args1 = [args[1][0], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][1], args[1][0].prototype instanceof GeneratorFunction)];
if (args[1][2]) {
args1.push(args[1][2]);
}
}
break;
}
}
break;
case 'apply':
if (args[1]) {
switch (args[1][0]) {
case Function:
args1 = [args[1][0], args[1][1], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][2])];
break;
case GeneratorFunction:
args1 = [args[1][0], args[1][1], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][2], true)];
break;
default:
if (args[1][0].prototype instanceof Function) {
args1 = [args[1][0], args[1][1], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][2], args[1][0].prototype instanceof GeneratorFunction)];
}
break;
}
}
break;
default:
break;
}
break;
case Function:
switch (args[0]) {
case 'apply':
args1 = [args[1][0], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][1])];
break;
case 'call':
args1 = [args[1][0], ...hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1].slice(1))];
break;
default:
break;
}
break;
case GeneratorFunction:
switch (args[0]) {
case 'apply':
args1 = [args[1][0], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][1], true)];
break;
case 'call':
args1 = [args[1][0], ...hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1].slice(1), true)];
break;
default:
break;
}
break;
default:
if (thisArg instanceof GeneratorFunction && args[0] === 'constructor') {
args1 = hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1], true);
}
else if (thisArg instanceof Function && args[0] === 'constructor') {
args1 = hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1]);
}
break;
}
break;
default:
if (typeof f === 'function') {
if (f.prototype instanceof Function && newTarget) {
args = hook.FunctionArguments('__hook__', [[context, {}]], 'method', args, f.prototype instanceof GeneratorFunction);
}
else if (newTarget === '') {
if (args[0] && Object.getPrototypeOf(args[0]) === Function) {
args = [ args[0], ...hook.FunctionArguments('__hook__', [[context, {}]], 'method', args.slice(1)) ];
}
}
}
break;
}
if (typeof f !== 'string') {
result = newTarget
? Reflect.construct(f, args)
: thisArg
? f.apply(thisArg, args)
: f(...args);
}
else {
// property access
switch (f) {
// getter
case '.':
case '[]':
result = thisArg[args[0]];
break;
// enumeration
case '*':
result = thisArg;
break;
// property existence
case 'in':
result = args[0] in thisArg;
break;
// funcation call
case '()':
result = thisArg[args[0]](...args1);
break;
// unary operators
case 'p++':
result = thisArg[args[0]]++;
break;
case '++p':
result = ++thisArg[args[0]];
break;
case 'p--':
result = thisArg[args[0]]--;
break;
case '--p':
result = --thisArg[args[0]];
break;
case 'delete':
result = delete thisArg[args[0]];
break;
// assignment operators
case '=':
result = thisArg[args[0]] = args[1];
break;
case '+=':
result = thisArg[args[0]] += args[1];
break;
case '-=':
result = thisArg[args[0]] -= args[1];
break;
case '*=':
result = thisArg[args[0]] *= args[1];
break;
case '/=':
result = thisArg[args[0]] /= args[1];
break;
case '%=':
result = thisArg[args[0]] %= args[1];
break;
case '**=':
result = thisArg[args[0]] **= args[1];
break;
case '<<=':
result = thisArg[args[0]] <<= args[1];
break;
case '>>=':
result = thisArg[args[0]] >>= args[1];
break;
case '>>>=':
result = thisArg[args[0]] >>>= args[1];
break;
case '&=':
result = thisArg[args[0]] &= args[1];
break;
case '^=':
result = thisArg[args[0]] ^= args[1];
break;
case '|=':
result = thisArg[args[0]] |= args[1];
break;
// LHS property access
case '.=':
result = { set ['='](v) { thisArg[args[0]] = v; }, get ['=']() { return thisArg[args[0]]; } };
break;
// strict mode operators prefixed with '#'
// getter
case '#.':
case '#[]':
result = StrictModeWrapper['#.'](thisArg, args[0]);
break;
// enumeration
case '#*':
result = StrictModeWrapper['#*'](thisArg);
break;
// property existence
case '#in':
result = StrictModeWrapper['#in'](thisArg, args[0]);
break;
// funcation call
case '#()':
result = StrictModeWrapper['#()'](thisArg, args[0], args1);
break;
// unary operators
case '#p++':
result = StrictModeWrapper['#p++'](thisArg, args[0]);
break;
case '#++p':
result = StrictModeWrapper['#++p'](thisArg, args[0]);
break;
case '#p--':
result = StrictModeWrapper['#p--'](thisArg, args[0]);
break;
case '#--p':
result = StrictModeWrapper['#--p'](thisArg, args[0]);
break;
case '#delete':
result = StrictModeWrapper['#delete'](thisArg, args[0]);
break;
// assignment operators
case '#=':
result = StrictModeWrapper['#='](thisArg, args[0], args[1]);
break;
case '#+=':
result = StrictModeWrapper['#+='](thisArg, args[0], args[1]);
break;
case '#-=':
result = StrictModeWrapper['#-='](thisArg, args[0], args[1]);
break;
case '#*=':
result = StrictModeWrapper['#*='](thisArg, args[0], args[1]);
break;
case '#/=':
result = StrictModeWrapper['#/='](thisArg, args[0], args[1]);
break;
case '#%=':
result = StrictModeWrapper['#%='](thisArg, args[0], args[1]);
break;
case '#**=':
result = StrictModeWrapper['#**='](thisArg, args[0], args[1]);
break;
case '#<<=':
result = StrictModeWrapper['#<<='](thisArg, args[0], args[1]);
break;
case '#>>=':
result = StrictModeWrapper['#>>='](thisArg, args[0], args[1]);
break;
case '#>>>=':
result = StrictModeWrapper['#>>>='](thisArg, args[0], args[1]);
break;
case '#&=':
result = StrictModeWrapper['#&='](thisArg, args[0], args[1]);
break;
case '#^=':
result = StrictModeWrapper['#^='](thisArg, args[0], args[1]);
break;
case '#|=':
result = StrictModeWrapper['#|='](thisArg, args[0], args[1]);
break;
// LHS property access
case '#.=':
result = StrictModeWrapper['#.='](thisArg, args[0]);
break;
// getter for super
case 's.':
case 's[]':
result = args[1](args[0]);
break;
// super method call
case 's()':
result = args[2](args[0]).apply(thisArg, args[1]);
break;
// unary operators for super
case 's++':
case '++s':
case 's--':
case '--s':
result = args[1].apply(thisArg, args);
break;
// assignment operators for super
case 's=':
case 's+=':
case 's-=':
case 's*=':
case 's/=':
case 's%=':
case 's**=':
case 's<<=':
case 's>>=':
case 's>>>=':
case 's&=':
case 's^=':
case 's|=':
result = args[2].apply(thisArg, args);
break;
// getter in 'with' statement body
case 'w.':
case 'w[]':
result = args[1]();
break;
// function call in 'with' statement body
case 'w()':
result = args[2](...args[1]);
break;
// constructor call in 'with' statement body
case 'wnew':
result = args[2](...args[1]);
break;
// unary operators in 'with' statement body
case 'w++':
case '++w':
case 'w--':
case '--w':
result = args[1]();
break;
// unary operators in 'with' statement body
case 'wtypeof':
case 'wdelete':
result = args[1]();
break;
// LHS value in 'with' statement body (__hook__('w.=', __with__, ['p', { set ['='](v) { p = v } } ], 'context', false)['='])
case 'w.=':
result = args[1];
break;
// assignment operators in 'with' statement body
case 'w=':
case 'w+=':
case 'w-=':
case 'w*=':
case 'w/=':
case 'w%=':
case 'w**=':
case 'w<<=':
case 'w>>=':
case 'w>>>=':
case 'w&=':
case 'w^=':
case 'w|=':
result = args[2](args[1]);
break;
// default (invalid operator)
default:
f(); // throw TypeError: f is not a function
result = null;
break;
}
}
return result;
}
// Example Hook Callback Function with Primitive Access Control
hashContext = { 'hash': 'context', ... }; // Generated from hook.preprocess initialContext[0][1]
trustedContext = { 'context': /trustedModules/, ... }; // Access Policies
window.__hook__ = function __hook__(f, thisArg, args, context, newTarget) {
console.log('hook:', context, args);
if (!hashContext[context] ||
!trustedContext[hashContext[context]] ||
!(new Error('').stack.match(trustedContext[hashContext[context]]))) {
// plus check thisArg, args, etc.
throw new Error('Permission Denied');
}
return newTarget
? Reflect.construct(f, args)
: thisArg
? f.apply(thisArg, args)
: f(...args);
}
If hooking is performed run-time in Service Worker, the entry HTML page must be loaded via Service Worker
so that no hook-targeted scripts are evaluated without hooking.
To achieve this, the static entry HTML has to be Encoded at build time by hook.serviceWorkerTransformers.encodeHTML(html)
.
# encode src/index.html to dist/index.html
hook --out dist/index.html src/index.html
<html>
<head>
<script src="../thin-hook/hook.min.js?version=1&no-hook=true&hook-name=__hook__&fallback-page=index-no-sw.html&hook-property=false&service-worker-ready=true"></script>
<!-- Hook Callback Function witout hooking properties -->
<script no-hook>
window.__hook__ = function __hook__(f, thisArg, args, context, newTarget) {
...
return newTarget
? Reflect.construct(f, args)
: thisArg
? f.apply(thisArg, args)
: f(...args);
}
</script><!-- end of mandatory no-hook scripts -->
<!-- comment --->
<script src="..."></script>
...
</html>
<html>
<head>
<script src="../thin-hook/hook.min.js?version=1&no-hook=true&hook-name=__hook__&fallback-page=index-no-sw.html&hook-property=false&service-worker-ready=false"></script></head></html>
<!-- Hook Callback Function without hooking properties -->
<script no-hook>
window.__hook__ = function __hook__(f, thisArg, args, context, newTarget) {
...
return newTarget
? Reflect.construct(f, args)
: thisArg
? f.apply(thisArg, args)
: f(...args);
}
</script><!--<C!-- end of mandatory no-hook scripts --C>
<C!-- comment --C>
<script src="..."></script>
...
</html>-->
</head></html>
is inserted between the first hook.min.js
script and the second no-hook script, which looks strange but is required for correct execution of no-hook scripts.</head></html>
is inserted at the end of mandatory no-hook scripts according to the normal HTML format, the page encounters the unexpected “hook is not defined” error, whose root cause is under investigation.{ m() {} }
)constructor
, super
, this
, new
)import
, export
);`${(v => v * v)(x)}`
)function *g() { yield X }
)a => a
, a => { return a; }
, a => ({ p: a })
)async function f() {}
, async method() {}
, async () => {}
)function f([ a = 1 ], { b = 2, x: c = 3 }) {}
)o.p
, o['p']
, o.p()
)
bower install --save thin-hook
npm install --save thin-hook
<!-- browserified along with espree and escodegen; minified -->
<script src="path/to/bower_components/thin-hook/hook.min.js"></script>
const hook = require('thin-hook/hook.js');
hook(code: string, hookName: string = '__hook__', initialContext: Array = [], contextGeneratorName: string = 'method', metaHooking: boolean = true, hookProperty: boolean = true, sourceMap: object = null, asynchronous: boolean = false, compact: boolean = false, hookGlobal: boolean = true, hookPrefix: string = '_p_', initialScope: object = null)
code
: input JavaScript as stringhookName
: name of hook callback functioninitialContext
: typically [ ['script.js', {}] ]
contextGeneratorName
: function property name in hook.contextGenerators
astPath = [ ['script.js', {}], ['root', rootAst], ['body', bodyAst], ..., [0, FunctionExpressionAst] ]
metaHooking
: Enable meta hooking (run-time hooking of metaprogramming) if truehookProperty
: Enable hooking of object property accessors and new operators if truesourceMap
: Source map parameter in an object. { pathname: 'path/to/script_source.js'}
Default: nullasynchronous
: Return a Promise if true. Default: falsecompact
: Generate compact code if true. Default: falsesourceMap
is disabled when compact
is truehookGlobal
: Hook global variable access. Must be enabled with hookProperty
. Default: truehookPrefix
: Prefix for hook.global()._p_GlobalVariable
proxy accessors. Default: _p_
hook.global()
return the global object with get/set
accessors for the prefixed nameinitialScope
: Initial scope object ({ vname: true, ... }
) for hooked eval scripts. Default: null$hook$
: $hook$ === hook
. Alias of hook
in hooked scriptshook.hookHtml(html: string, hookName, url, cors, contextGenerator, contextGeneratorScripts, isDecoded, metaHooking = true, scriptOffset = 0, _hookProperty = true, asynchronous = false)
hook.__hook__(f: function or string, thisArg: object, args: Array, context: string, newTarget: new.target meta property)
f
:function
: target function to hookstring
: property operation to hook.
: get property (o.prop
)*
: iterate over (for (p in o)
, for (p of o)
)in
: property existence ('p' in o
)()
: function call (o.func()
)=
, +=
, …: assignment operation (o.prop = value
)p++
, ++p
, p--
, --p
: postfixed/prefixed increment/decrement operation (o.prop++
)delete
: delete operation (delete o.prop
)s.
: get property of super (super.prop
)s()
: call super method (super.method()
)s=
, s+=
, …: assignment operation for super (super.prop = value
)s++
, ++s
, s--
, --s
: postfixed/prefixed increment/decrement operation for super (super.prop++
)w.
, w=
, w()
, w++
, …: operations on variables in within with
statementsthisArg
: this
object for the function or the operationargs
:[ property ]
for property access operations[ property, value ]
for property assignment operations[ property, [...args] ]
for function call operationscontext
: context in the scriptnewTarget
: new.target
meta property for constructor calls;true
for new callsnew
operations for faster detection of the operationsfalse
for with
statement calls0
for function callsundefined
for other callshook.__hook_except_properties__(f, thisArg, args, context, newTarget)
hook.hookCallbackCompatibilityTest(__hook__ = window[hookName], throwError = true, checkTypeError = true)
window.__hook__ = function __hook__ (...) {}; hook.hookCallbackCompatibilityTest();
false
is returned on a test failure if throwError = false
checkTypeError = false
hook.contextGenerators
: object. Context Generator Functionsnull()
: context as ''
astPath(astPath: Array)
: context as 'script.js,[root]Program,body,astType,...'
method(astPath: Array)
: context as 'script.js,Class,Method'
with caching, including computed method variable namecachedMethod(astPath: Array)
: alias for method
cachedMethodDebug(astPath: Array)
: context as 'script.js,Class,Method'
, comparing contexts with those by “oldMethod” in console.warn() messagesoldMethod(astPath: Array)
: context as 'script.js,Class,Method'
for compatibilityhook.$(symbolToContext = __hook__, contexts)
: context symbol generator function used in hooked scripts to generate symbols corresponding to given contextsconst __context_mapper__ = $hook$.$(__hook__, [ 'examples/example2.js,C', ... ]);
__context_mapper__
: Array
of symbol contexts__context_mapper__
is actually __ + hex(sha256(topContextOfScript + code)) + __
__context_mapper__[N]
: the symbol context corresponding to the string context contexts[N]
__hook__[__context_mapper__[N]]
is set as contexts[N]
so that __hook__
can convert symbol contexts to their corresponding string contextshook()
preprocessinghook.global(hookCallback: function = hookName, context: string, name: string, type: string)._p_name
: hooked global variable accessor when hookGlobal
is truetype
: one of 'var', 'function', 'let', 'const', 'class', 'get', 'set', 'delete', 'typeof'
hook.Function(hookName, initialContext: Array = [['Function', {}]], contextGeneratorName)
: hooked Function constructor for use in hook callback function __hook__
(new (hook.Function('__hook__', [['window,Function', {}]], 'method'))('return function f() {}'))()
window.Function
for better transparency (now commented out in the demo/hook-native-api.js
)__hook__
) insteadhook.FunctionArguments(hookName, initialContext: Array = [['Function', {}]], contextGeneratorName = 'method', args, isGenerator = false)
: generate hooked Function arguments to hand to Function constructor for use in hook callback function __hook__
hook.FunctionArguments('__hook__', [['window,Function', {}]], 'method', ['return function f() {}'])
args
in a cloned Array
hook.eval(hookName, initialContext: Array = [['eval', {}]], contextGeneratorName)
: hooked eval functionhook.eval('__hook__', [['eval', {}]], 'method'))('1 + 2', (script, eval) => eval(script))
eval
function via hook.hook(hook.eval(...))
, the evaluation is bound to the global scope unless the wrapper arrow function (script, eval) => eval(script)
is defined in the local scope and specifed as the second argument of each eval()
callhook.setTimeout(hookName, initialContext: Array = [['setTimeout', {}]], contextGeneratorName)
: hooked setTimeout functionhook.setInterval(hookName, initialContext: Array = [['setInterval', {}]], contextGeneratorName)
: hooked setInterval functionhook.Node(hookName, initialContext: Array = [['Node', {}]], contextGeneratorName)
: hook textContent
propertyset textContent
: hooked with context ‘ClassName,set textContent’hook.Element(hookName, initialContext: Array = [['Element', {}]], contextGeneratorName)
: hook setAttribute
functionsetAttribute('onXX', '{script in attribute}')
: Script in onXX handler attribute is hookedsetAttribute('href', 'javascript:{script in URL}')
: Script in URL "javascript:{script in URL}"
is hookedhook.HTMLScriptElement(hookName, initialContext: Array = [['HTMLScriptElement', {}]], contextGeneratorName)
: HTMLScriptElement with hooked propertiesHTMLScriptElement
class is the same object as the native one. hook.Node
and hook.Element
are called internally.set textContent
: Script in textContent
is hooked if type
is a JavaScript MIME type. Node.textContent
is hooked as well.innerHTML
/outerHTML
/text
properties are NOT executed, while text
should be executed according to the standards.set type
: Script in this.textContent
is hooked if type
is a JavaScript MIME type.setAttribute('type', mimeType)
: Script in this.textContent
is hooked if mimeType
is a JavaScript MIME type. Element.setAttribute
is hooked as well.hook.HTMLAnchorElement(hookName, initialContext: Array = [['HTMLAnchorElement', {}]]), contextGeneratorName)
: HTMLAnchorElement with hooked href propertyset href
: Script in URL "javascript:{script in URL}"
is hookedhook.HTMLAreaElement(hookName, initialContext: Array = [['HTMLAreaElement', {}]]), contextGeneratorName)
: HTMLAreaElement with hooked href propertyset href
: Script in URL "javascript:{script in URL}"
is hookedhook.Document(hookName, initialContext: Array = [['Document', {}]], contextGeneratorName)
: hook write
functionwrite('<sc' + 'ript>{script in string}</sc' + 'ript>')
: Script in HTML fragment is hookedhook.with(scope: Object, ...scopes: Array of Object)
: Hook with
statement scope objectwith (hook.with(obj, { v1: true, v2: true, ...})) {}
hook.importScripts()
: return hooked importScripts
function for Workers, invalidating extensions other than .js
and .mjs
hook.hook(target: Class, ...)
: hook platform global object with target
['Function','setTimeout','setInterval',...].forEach(name => hook.hook(hook.Function('__hook__', [[name, {}]], 'method'))
hook.serviceWorkerHandlers
: Service Worker event handlersinstall
: ‘install’ event handler. Set version from the version
parameteractivate
: ‘activate’ event handler. Clear caches of old versions.message
: ‘message’ event handler.'channel'
message: Transfer MessageChannel port objects for hook workers from the main document to the Service Worker at initialization'unload'
message: Trigger unloading of hook workers'coverage'
message: Transfer __coverage__
instanbul coverage object for the Service Worker to the main document to collect code coverage in test/hook.min.js
['plugin', 'pluginId', ...params ]
message: Transfer a message to the target plugin identified by 'pluginId'
. The target plugin must add its own event listener to handle the message.['plugin', 'pluginId:enqueue', ...params ]
: When the pluginId
ends with :enqueue
, events with posted messages are enqueued to hook.parameters.messageQueues['pluginId:enqueue'] = []
even before plugins are loaded into the Service Workerevent.ports[0].postMessage()
with a dummy response message generated by cloning the posted message and appending ':enqueued'
such as ['plugin', 'pluginId:enqueue', ...params, ':enqueued' ]
':dequeued'
to the queue to stop further enqueueing. For example, the queue []
changes as follows:[ event1 ]
':dequeued'
: [ event1, ':dequeued' ]
[ ':dequeued' ]
hook.parameters.messageQueues['pluginId:enqueue']
may NOT exist when the plugin is loaded. So the plugin must create its own queue if it has not been created.fetch
: ‘fetch’ event handler. Cache hooked JavaScripts and HTMLs except for the main page loading hook.min.js
<script src="thin-hook/hook.min.js?version=1&sw-root=/&no-hook=true&hook-name=__hook__&discard-hook-errors=true&fallback-page=index-no-sw.html&hook-property=true&service-worker-ready=true"></script>
: arguments from the pageversion
: default 1
. Service Worker cache version. Old caches are flushed when the version is changed in the main page and reloaded. Service Worker is updated when the controlled page is detached after the reloading.sw-root
: optional. Set Service Worker scopehook-name
: default __hook__
. hook callback function namecontext-generator-name
: default method
. context generator callback function namediscard-hook-errors
: true
if errors in hooking are ignored and the original contents are provided. Default: true
fallback-page
: fallback page to land if Service Worker is not available in the browserno-hook-authorization
: Optional. CSV of no-hook authorization tickets for no-hook scripts. Typically for ticket of no-hook authorization script itself.hook.parameters.noHookAuthorizationPreValidated
object in Service Workerlog-no-hook-authorization
to log authorization in consoleno-hook-authorization
must not exist in learning mode with hook.parameters.noHookAuthorization['*'] === true
hook.parameters.noHookAuthorization['*']
no-hook-authorization
parameter from hook.min.jsversion
parameter for hook.min.jsno-hook-authorization
parameter for hook.min.js with a dummy valueversion
parameter for hook.min.jsno-hook-authorization
parameterversion
parameter for hook.min.jshook-property
: hookProperty
parameter. true
if property accessors are hooked. The value affects the default value of the hookProperty
parameter for hook()
hook-global
: hookGlobal
parameter. true
if global variables are hooked. The value affects the default value of the hookGlobal
parameter for hook()
hook-prefix
: hookPrefix
parameter. Prefix accessor names of hook.global()._p_GlobalVariableName
with the value. Default: _p_
compact
: compact
parameter. Generate compact code if true
. The value affects the default value of the compact
parameter for hook()
service-worker-ready
: true
if the entry HTML page is decoded; false
if encoded. This parameter must be at the end of the URL<script src="script.js?no-hook=true"></script>
: skip hooking for the source script<script no-hook>...</script>
: skip hooking for the embedded script<script context-generator>
: register a custom context generator for both Service Worker and browser document<script context-generator no-hook>hook.contextGenerators.custom = function (astPath) {...}</script>
: embedded script<script context-generator src="custom-context-generator.js?no-hook=true"></script>
: with src URLhook.min.js
for Service Workerversion
variable: cache name as a string version_{version number}
'version_' + new URL(location.href).searchParams.get('version')
might be incorrect since Service Worker for the old version before version upgrading might still be running for the new version. In contrast, 'version_' + new URL(document.querySelector('script').src).searchParams.get('version')
in the main document is always up-to-date.hook.parameters.cors = [ 'cors_url_1', 'cors_url_2', ... ]
: specify CORS script URLshook.parameters.cors = [ (url) => url.match(/cors_url_pattern/), ... ]
: specify CORS script URL detector function(s)hook.parameters.opaque = [ 'opaque_url_1', 'opaque_url_2', ... ]
: specify authorized opaque URLshook.parameters.opaque = [ (url) => url.match(/opaque_url_pattern/), ... ]
: specify authorized opaque URL detector function(s)no-hook
Authorization Tickets:hook.parameters.noHookAuthorization = { '{sha-256 hex hash for authorized no-hook script}': true, ... }
: Set keys from hook.parameters.noHookAuthorizationPassed
in both Document and Service Worker threadshook.parameters.noHookAuthorization = { '*': true }
: learning mode to detect authorization ticketsno-hook
scripts:hook.parameters.noHook = [ 'no_hook_url_1', 'no_hook_url_2', ... ]
: specify no-hook
script URLshook.parameters.noHook = [ (url: URL) => !!url.href.match(/{no-hook URL pattern}/), ... ]
: specify no-hook
script URL detector function(s)hook.parameters.sourceMap = [ 'source_map_target_url_1', 'source_map_target_url_2', ... ]
: specify source map target script URLshook.parameters.sourceMap = [ (url: URL) => !!url.href.match(/{source map target URL pattern}/), ... ]
: specify source map target script URL detector function(s)hook.parameters.hookWorker = 'hook-worker.js?no-hook=true'
: specify hook worker script URLif (typeof self === 'object' && self instanceof 'ServiceWorkerGlobalScope') { self.addEventListener('{event_type}', function handler(event) {...})}
hook.parameters.baseURI
: Set in demo/bootstrap.js
hook.parameters.emptyDocumentUrl = new URL('./empty-document.html', baseURI);
: Set in demo/bootstrap.js
. <iframe src="empty-document.html?url=https://host/path.html,iframe">
to specify context in iframe documenthook.parameters.bootstrap = "<script>frameElement.dispatchEvent(new Event('srcdoc-load'))</script>";
: Set in demo/bootstrap.js
srcdoc
to dispatch srcdoc-load
event to onload
handlerhook.parameters.onloadWrapper = "event.target.addEventListener('srcdoc-load', () => { $onload$ })";
: Set in demo/bootstrap.js
srcdoc-load
event and trigger the original onload
scriptaddEventListener('load', handler)
is currently called BEFORE the document from srcdoc
is loaded and srcdoc-load
event is fired.hook.parameters.virtualBlobUrlTargetType = new Map([['text/html', 'file.html'],['text/javascript', 'file.js'],['image/svg+xml', 'file.svg']]);
: Set in demo/bootstrap.js
to specify target MIME types and their corresponding virtual Blob URL file nameshook.parameters.virtualBlobBaseUrl = null;//new URL('blob/', hook.parameters.baseURI).href;
: Set in demo/bootstrap.js
to specify the base URL for virtual Blob URLsblob
//origin.site/abcd...1234
for text/html
Blob objecthttps://origin.site/entry/blob/file.html?bloburl=blob
//origin.site/abcd...1234
<embed>
and <object>
elementshook.parameters.hangUpOnEmbedAndObjectElement = false;
: Set in demo/bootstrap.js
true
, the application hangs up on encountering activities by <object>
and <embed>
elementshook.parameters.mutationObserver
and hook.parameters.mutationObserverConfig
must be set in demo/hook-callback.js
hook.parameters.emptySvg = '<?xml version="1.0"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1px" height="1px"><script>location = "$location$";</script></svg>';
hook.parameters.hangUpOnEmbedAndObjectElement = true
, the SVG loads about:blank
hook.parameters.bootstrapSvgScripts = '<script xlink:href="URL?params"></script>...'
hook.parameters.omitSuperfluousClosingHtmlTags = true
: true
to omit superfluous closing tags; Set in demo/bootstrap.js
false
to be compatible with old versionsfalse
, SVG images with case-sensitive tags and attributes can be broken.true
, the output should have minimal required changes from the original HTML or SVG/path/script.js,script@{pos}
, which can vary with this flag set as true
hook.parameters.checkRequest = async function (event, response, cache) { /* check request */ return response ; }
: response
- cached response if exists; See demo/disable-devtools.js
hook.parameters.checkResponse = async function (event, request, response, cache) { /* check response */ return response ; }
: response
- just fetched response; Not called if a cache response existshook.parameters.preServiceWorkerTasks
: The first task is checked after DOMContentLoaded
event; Therefore, the first task Promise
must be pushed before DOMContentLoaded
event "skipServiceWorkerRegistration"
, the default Service Worker registration processes are skipped and the last task takes the responsiblity of the Service Worker registration and reloading the entry page. Even after the Service Worker registration completed and the page is reloaded, this "skipServiceWorkerRegistration"
value is effective for the task so that hook.min.js
can complete remaining tasks such as starting the ping service and the hook workers.Promise
: A single Promise
object; Equivalent to [ Promise ]
;[ Promise, Promise, ...]
: Array
of Promise
objectsAsync Iterable
: Async Iterator
implementing tasks[Symbol.asyncIterator]
protocolhook.parameters.preServiceWorkerTasks
hook.parameters.onPreServiceWorkerTasksError = async function onError(exception) {}
: Asynchronous function to handle the exceptionwindow.location = 'about:blank';
hook.parameters.decodeEntryHtml = async function decodeEntryHtml(event, request, response, cache, original, decoded)
event: FetchEvent
: Event for the fetch requestrequest: Request
: Request object that fetched the contentresponse: Response
: Response object of the fetchcache: Cache
: Cache object for the current versionoriginal: String
: original entry page HTMLdecoded: String
: = hook.serviceWorkerTransformers.decodeHtml(original)
String
decoded entry page HTML to respond to the documentoriginal
or decoded
while it can also modify the content depending on the situation.hook.parameters.significantHeaders = { "Header-Name": true }
hook.parameters.cacheableContentTypes = { "text/css": true, "image/png": true, ... }
text/html
, text/javascript
, image/svg+xml
must not be included herehook.parameters.validateCacheableUrl = function (url, contentType)
url: String
: target URL to validatecontentType: String
: normalized Content-Type without charseturl
with contentType
is cacheablecontentType
values within hook.parameters.cacheableContentTypes
are cacheablehook.parameters.appPathRoot = '/';
- The app assets are under location.origin + hook.parameters.appPathRoot
hook.parameters.scriptHashes = { "SHA256(authorized inline script)": "context", ... }
- List of hashes for authorized inline scriptshook.parameters.integrity = { "URL path": "base64(SHA256(response data))", ... }
- List of integrity for static contentshook.parameters.mutationObserver = new MutationObserver(observerCallback);
- MutationObserver
object set in demo/hook-callback.js
hook.parameters.mutationObserverConfig = { childList: true, subtree: true, attributes: true, attributeOldValue: true, characterData: true, characterDataOldValue: true, };
- Configuration options for hook.parameters.mutationObserver.observe(options)
set in demo/hook-callback.js
Node.attachShadow()
to track mutations in every shadow DOM as well as for all document objects of windows and frameshook.parameters.innerHTMLTracker = function (node, value, processed) {}
: Set in demo/hook-callback.js
for mutation detectionElement.innerHTML
operation before performing ithook.parameters.importMapsJson = "{ JSON string for Import Maps }"
: Optional import maps object in JSON string.hook.parameters.importMapper(specifier, scriptURL)
: Wrapper function for import maps. Resolve module specifier
from scriptURL
hook.parameters.importMapper = null
hook.parameters.moduleDependencies = {}
: Optional object to dump module dependencies for hooked modulesService-Worker-Allowed
HTTP response header must have an appropriate scope for the target applicationcors=true
parameter: CORS script, e.g., <script src="https://cross.origin.host/path/script.js?cors=true"></script>
hook.serviceWorkerTransformers
:encodeHtml(html: string)
: encode HTML for Service Worker<!-- end of mandatory no-hook scripts -->
: insert this exact marker as a comment so that all mandatory no-hook scripts before the marker in the HTML of the entry document can be executed even at the first load without Service Workerno-hook-authorization
hashes are NOT effective at the first loaddecodeHtml(html: string)
: decode encoded HTML for Service Workerhook.hookWorkerHandler(event)
: onmessage handler for Hook Workersonmessage = hook.hookWorkerHandler
in Hook Worker scripthook.registerServiceWorker(fallbackUrl: string = './index-no-service-worker.html', reloadTimeout: number = 500, inactiveReloadTimeout: number = 1000)
:hook.min.js
on browsersfallbackUrl
: fallback URL for browsers without Service WorkerreloadTimeout
: default: 500 (ms). Timeout to reload the page when no Service Worker is detectedinactiveReloadTimeout
: default: 1000 (ms). Timeout to reload the page when inactive (waiting, installing) Service Worker is detected. When a state change of the Service Worker instance is detected, the page is reloaded immediately even before the timeout.utils
: UtilitiescreateHash
: Synchronous SHA hash generator collections from sha.jsHTMLParser
: HTML parser from htmlparser2importMaps
: Forked reference implementation of Import mapsparseFromString(importMapsJsonString, baseURL)
: Parser of import maps JSON string at baseURL
. Return parsedImportMap
object for resolve()
resolve(specifier, parsedImportMap, scriptURL)
: Resolver of specifier
for scriptURL
based on parsedImportMap
demo/
, but fully customizable for any target applications<script context-generator src="no-hook-authorization.js?no-hook=true"></script>
hook.parameters.noHookAuthorization = { "hex sha256 digest for no-hook script": true, ... }
update-no-hook-authorization
gulp taskno-hook-authorization.js
script itself has to be set as a parameter for hook.min.js
<script src="../../thin-hook/hook.min.js?version=496&no-hook-authorization=6a83335a7630118516213f52715a24520efc7030b3562291e92a06482894b95e&service-worker-ready=false"></script>
update-no-hook-authorization-in-html
gulp taskhook.parameters.sourceMap = [...]
hook.parameters.hookWorker = 'hook-worker.js?no-hook=true';
<script context-generator src="integrity.js?no-hook=true"></script>
integrityService.js
x-cache-*
headers<script context-generator src="disable-devtools.js?no-hook=true"></script>
about:blank
when the user tries to open Developer Toolsabout:blank
when the user tries to inspect a source code of the pagesconst devtoolsDisabled = true
: Use false
and rebuild with gulp demo
to enable Dev ToolstargetConfig.mode.devtoolsDisabled
in demo-config/config.js
<script context-generator src="context-generator.js?no-hook=true"></script>
hook.contextGenerators.hash
: an example custom context generator (not used for demo)hook.contextGenerators.method2
: an example custom context generator (not used for demo)Object.freeze(hook.contextGenerators)
<script context-generator src='bootstrap.js?no-hook=true'></script>
hook.parameters.emptyDocumentUrl
hook.parameters.bootstrap
hook.parameters.onloadWrapper
hook.parameters.virtualBlobUrlTargetType
hook.parameters.virtualBlobBaseUrl
hook.parameters.hangUpOnEmbedAndObjectElement
hook.parameters.emptySvg
hook.parameters.bootstrapSvgScripts
hook.parameters.noHookAuthorizationParameter
: Value of hook.min.js?no-hook-authorization
parameter used in hook-callback.js
hook.parameters.noHookAuthorizationFailed = {}
hook.parameters.noHookAuthorizationPassed = {}
<script context-generator no-hook>hook.parameters.* ...</script>
hook.parameters.cors
hook.parameters.opaque
hook.parameters.worker
(Ineffective and unused for now)<script context-generator src="cache-bundle.js?no-hook=true&authorization=..."></script>
Features
cache-bundle.json
and store the contents into caches
{ "version": "version_XXX", "same origin URL path (absolute)": "text data", ..., "absolute URL": "text data", ... }
.js
: application/json
.html
: text/html
.json
: application/json
.svg
: image/svg+xml
demo/cache-bundle.json
"url?param=2": Object
"Location": "url?param=1"
- link to the other content to eliminate redundant identical body data for multiple URLs"Location"
exists, other metadata entries are ignored"Location": "data:image/jpeg;base64,..."
- encoded body data for non-textual contents"Location"
appears only once in a metadata object, of course"Content-Type": "text/xml"
- MIME type"body": "body in string"
- content body"Other-Headers": "header value"
- additional significant HTTP headers specified in hook.parameters.significantHeaders
cache-bundle.json
from caches
and upload the data to saveURL (errorReport.json
) if the entry page is invoked with ?cache-bundle=save
parameternpm run upload
with cacheBundleUploadService.js
to receive and save cache-bundle.json
{ "type": "cache-bundle.json", "data": "stringified cache-bundle.json" }
cache-bundle.json
cacheBundleGeneration.js
via puppeteer
cache-bundle
gulp taskcache-bundle.json
at build timecache-bundle-automation-json
gulp task"version": "version_123"
: version obtained via get-version
gulp task"https://thin-hook.localhost.localdomain/automation.json":
: JSON.stringify()
with the object with the following properties"state": "init"
: update state in the script to perform operations including reloading"serverSecret": serverSecret
: one-time build-time-only secret for validating cache-automation.js
script"script": cacheAutomationScript
: contents of cache-automation.js
scriptcache-automation.js
: script for collecting caches by automatically navigating the target applicationcache-automation.js
script is hooked with the context https://thin-hook.localhost.localdomain/automation.json,*
cache-automation.js
cache-automation.js
executioncache-automation.js
executioncache-bundle.json
Configurations
const enableCacheBundle = true
: Use false
and rebuild with gulp demo
to disable cache-bundle
cache-bundle.json
hook.parameters.significantHeaders = { "Header-Name": true }
: optionalhook.parameters.cacheableContentTypes = { "text/css": true, "image/png": true, ... }
: optionalhook.parameters.validateCacheableUrl = function (url, contentType)
: optionalconst cacheBundleURL = new URL('cache-bundle.json', hook.parameters.baseURI);
const saveURL = new URL('errorReport.json', hook.parameters.baseURI);
?authorization=
: hex(sha256(serverSecret + cache-automation.js script))
encode-demo-html
gulp taskcache-bundle.json
cache-automation.js
must be fully customized for the target applicationcache-automation.js
with the context https://thin-hook.localhost.localdomain/automation.json,*
<script src="hook-callback.js?no-hook=true"></script>
contextStack
with Stack
class objectStack
class object is a brancheable linked list with push/pop
operationsStack
is not utilized for nowhook.hookCallbackCompatibilityTest()
blob:
URLs except for downloading to local fileshooked = hook[name](Symbol.for('__hook__'), [[name, { random: name === 'Node' }]], 'method')
Object.defineProperty(_global, name, { value: hooked, configurable: true, enumerable: false, writable: false });
eval
setTimeout
setInterval
Node
Element
HTMLScriptElement
HTMLIFrameElement
HTMLObjectElement
HTMLEmbedElement
HTMLAnchorElement
HTMLAreaElement
Document
importScripts
undefined
on prohibited global object accessabout:blank
on prohibited global object access__hook__
: hook callback functionObject.defineProperty(_global, '__hook__', { configurable: false, enumerable: false, writable: false, value: hookCallbacks.__hook__ });
hookCallbacks.__hook__
: full features (acl + contextStack + object access graph)hookCallbacks.__hook__acl
: acl only (acl + contextStack) - defaulthookCallbacks.__hook__min
: minimal (no acl)contextNormalizer
and acl
demo-config/policy/policy.js
and included policy moduleshook.parameters.mutationObserver = new MutationObserver(observerCallback);
- MutationObserver
object set in demo/hook-callback.js
hook.parameters.mutationObserverConfig = { childList: true, subtree: true, attributes: true, attributeOldValue: true, characterData: true, characterDataOldValue: true, };
- Configuration options for hook.parameters.mutationObserver.observe(options)
set in demo/hook-callback.js
hook.parameters.innerHTMLTracker = function (node, value, processed) {}
- Tracker callback to detect coming DOM mutations from setting Element.innerHTML
const detectDOMIntrusion = true;
- Use true
to detect DOM intrusionconst messagesOnUnauthorizedMutation = { en: 'Blocked on Browser Extensions' };
- Alert messages on DOM intrusion detection, indexed for navigator.language
const enableDebugging = false
: Use true
to enable debugging by disabling forced redirection to about:blank
on prohibited global object accesstargetConfig.mode.enableDebugging
in demo-config/config.js
const wildcardWhitelist
: Array
of RegExp
for Chrome browser’s new Error().stack
formatdemo-config/policy/wildcardWhitelist.js
new RegExp('^at (.* [(])?' + origin + '/components/'), // trust the site contents including other components
new RegExp('^at ([^(]* [(])?' + 'https://cdnjs.cloudflare.com/ajax/libs/vis/4[.]18[.]1/vis[.]min[.]js'),
new RegExp('^at ([^(]* [(])?' + 'https://www.gstatic.com/charts/loader[.]js'),
const excludes = new Set() : { 'window.Math' }
: exclude Math
objectMath
object properties must be wrapped with wrapGlobalProperty
function<script context-generator src="script-hashes.js?no-hook=true&service-worker-ready=false"></script>
gulp script-hashes
task and inserted into cache-bundle.json
with the key SCRIPT_HASHES_PSEUDO_URL https://thin-hook.localhost.localdomain/script-hashes.json
service-worker-ready=false
while it is copied from cache-bundle.json
if service-worker-ready=true
hook.parameters.scriptHashes
integrity
attribute requires 2 integrity values for both service-worker-ready=false
and service-worker-ready=true
gulp script-hashes-integrity
task and inserted into the entry page, i.e., original-index.html
and index.html
service-worker-ready=false
for the entry page, which is automatically converted to service-worker-ready=true
after preprocessedservice-worker-ready=true
for other HTML pages including empty-document.html
script-hashes
, script-hashes-integrity
<script src="content-loader.js?no-hook=true"></script>
empty-document.html
for iframe
documentsiframe
element is automatically configured in preprocessing HTML contentscontent=base64URL(encodeURIComponent(HTML))
iframe
document via document.write(HTML)
after preprocessingblob=encodeURIComponent(BlobURL)
iframe
document as HTML via document.write(fetch(Blob))
after preprocessing if the blob type is text/html
iframe
document as a plain text via data URL if the blob type is text/plain
image/svg+xml
iframe
document as data URL<script src="wrap-globals.js?no-hook=true"></script>
hook-callback.js
hook-callback.js
window[Symbol.for('wrapGlobalProperty')]()
to wrap global objectshook-callback.js
const excludes = new Set(); [ 'Math' ].forEach(name => excludes.add(name));
window.*
const URL = window.URL, RegExp = window.RegExp, ...
contextStack
is empty, the global object is accessed outside of hooked scripts and thus new Error().stack
has to be analyzed, which is an extremely heavy operationcontextStack
is not empty, the global object is accessed within a hooked script, whose access can be controlled via ACLcontextStack
operations are relatively lightweight without performance degradation on deep call stack<script src="mark-parsed.js?no-hook=true"></script>
node[Symbol.for('parsed')] = true
at the end of HTML body to filter out valid DOM mutations from invalid onesiframe
documents, dispatch srcdoc-load
event for the containing frameElement
hook.parameters.bootstrap
for the iframe
document wrapped via hook.parameters.emptyDocumentUrl
Server-side scripts and components configured for the demo but fully customizable for the target application
demo-backend/demoServer.js
Back-end server for the demo. TBD
demo-backend/errorReportService.js
Handler for demo/errorReport.json
POST requests
demo-backend/cacheBundleGeneration.js
Used at build time to automate generation of cache-bundle.json
via puppeteer
demo-backend/cacheBundleUploadService.js
Formerly used at build time to automate uploading of cache-bundle.json
via a POST request
demo-backend/postHtml.js
Express middleware for demoServer.js
to handle demo/postHtml
. This should be unnecessary and should not be used except for verification of HTML via a POST request.
demo-backend/integrityService.js
Express middleware for demoServer.js
to provide integrity and double encryption of body data
demo-backend/whitelist.json
- list of URL paths which are allowed to access without encryptiondemo-backend/blacklist.json
- list of URL paths which are not allowed to access; namely demo/index.html
gulp encode-demo-html
task by parsing the entry page HTMLdemo-backend/integrity-service-helpers/build/release/native.node
Node addon package compiled from the C++ source binding.cpp
to provide the following functions
rsa_oaep_decrypt(ArrayBuffer encrypted, String private_key_pem)
- Decrypt ArrayBuffer data by RSA-OAEP-SHA256 with a String private key in PEM format via openssl
demo-backend/validationService.js
When invoked as a CLI script, it provides the validation server for ClientIntegrity.browserHash
. TBD
demo-backend/validation-console/dist/
is served at its HTTPS rootdemo-keys/demoCA/${process.env["VALIDATION_HOST"]}.{key|crt}
is used for HTTPS server. Defaults to localhost:8082
demo-keys/demoCA/client.{key|crt}
are used for client certificate authenticationWhen imported as a package, it provides the client API for the validation server. TBD
demo-keys/demoCA/client.{key|crt}
are used for client certificate authenticationdemo-backend/validation-console/dist/
Validation Console GUI served by demo-backend/validationService.js
. TBD
demo-keys/generate_cert.sh
Script to generate certificates in demo-keys/demoCA/
demo-keys/keys.json
Key pairs and secret keys are stored for the application version.
{
"version": "version_668", // application version
"rsa-private-key.pem": "RSA PRIVATE KEY in PEM",
"rsa-public-key.pem": "RSA PUBLIC KEY in PEM",
"ecdsa-private-key.pem": "ECDSA PRIVATE KEY in PEM",
"ecdsa-public-key.pem": "ECDSA PUBLIC KEY in PEM",
"session-id-aes-key": "base64(random(32 bytes))",
"session-id-aes-iv": "base64(random(12 bytes))",
"scriptsHashHex": "hex(ClientIntegrity.scriptsHash)",
"htmlHashHex": "hex(ClientIntegrity.htmlHash)"
}
TBD
{
"scripts": {
"test": "wct",
"build": "gulp",
"demo": "run-p -l demoServer errorReportService validationService",
"debug": "run-p -l debugServer errorReportService validationService",
"https": "run-p -l httpsServer errorReportService validationService",
"upload": "run-p -l buildServer cacheBundleUploadService",
"cache-bundle": "run-p -r -l buildServer cacheBundleUploadService cacheBundleGeneration",
"updateHtmlHash": "run-p -r -l buildServer cacheBundleUploadService loadOnly",
"buildServer": "node demo-backend/demoServer.js -p 8080 -m build -P https -H \"localhost:8080\"",
"demoServer": "node demo-backend/demoServer.js -p 8080 -m server -c 4 -H \"${SERVER_HOST}\"",
"httpsServer": "node demo-backend/demoServer.js -p 8080 -m server -c 4 -P https -H \"${SERVER_HOST}:8080\"",
"debugServer": "node --inspect-brk=0.0.0.0:9229 demo-backend/demoServer.js -p 8080 -m debug -c 1 -H \"${SERVER_HOST}\"",
"postHtml": "run-p -l postHtmlServer errorReportService",
"postHtmlServer": "node demo-backend/demoServer.js -p 8080 -m server -c 4 -H \"${SERVER_HOST}\" --middleware ./postHtml.js",
"errorReportService": "node demo-backend/errorReportService.js -p 8081",
"validationService": "node demo-backend/validationService.js -p 8082 -m server -H \"${VALIDATION_HOST}\"",
"integrity-service-helpers": "cd demo-backend/integrity-service-helpers && npm install",
"validation-console": "cd demo-backend/validation-console && npm ci && npm run build",
"demo-certificates": "cd demo-keys && ./generate_cert.sh ",
"clean-demo-certificates": "cd demo-keys && rm -riv demoCA",
"cacheBundleUploadService": "node demo-backend/cacheBundleUploadService.js",
"cacheBundleGeneration": "node demo-backend/cacheBundleGeneration.js",
"loadOnly": "node demo-backend/cacheBundleGeneration.js loadOnly",
"test:attack": "run-p -r -l buildServer cacheBundleUploadService puppeteerAttackTest",
"puppeteerAttackTest": "node test/puppeteerAttackTest.js",
"demo-frontend-modules": "cd demo/ && npm install",
"demo-frontend-modules-locked": "cd demo/ && npm ci"
}
}
${SERVER_HOST}
environment variableHTTPS server host name for the application. Defaults to localhost
${VALIDATION_HOST}
environment variableHTTPS server host name at port 8082 for Validation Console and Validation Service API. Defaults to localhost
npm test
Run hook tests
npm run build
Build hook.min.js
and the demo via gulp
npm run demo
Serve the demo from demo-frontend/
at https://${SERVER_HOST}/components/thin-hook/demo/
via nginx proxing to http://localhost:8080
npm run debug
Serve the demo from demo/
at https://${SERVER_HOST}/components/thin-hook/demo/
via nginx proxying to http://localhost:8080
npm run https
Serve the demo from demo-frontend/
at https://${SERVER_HOST}:8080/components/thin-hook/demo/
with the key pair demo-keys/demoCA/${SERVER_HOST}.{key|crt}
npm run upload
Formerly used to upload cache-bundle.json
via demo-backend/cacheBundleUploadService.js
at build time
npm run cache-bundle
Called from gulp cache-bundle-automation
task to automate building of cache-bundle.json
at build time
npm run updateHtmlHash
Called from gulp update-html-hash
task to update demo-keys/keys.json
for "htmlHashHex"
of the entry page HTML after the integrity
attribute of <script src="script-hashes.js">
is updated in gulp script-hashes-integrity
task
npm run buildServer
Called from npm run cache-bundle
to tonvoke demoServer.js
in build
mode at build time
npm run demoServer
Called from npm run demo
to invoke demoServer.js
in server
mode without TLS
npm run httpsServer
Called from npm run https
to invoke demoServer.js
in server
mode with TLS
npm run debugServer
Called from npm run debug
to invoke demoServer.js
in debug
mode attached by Node.js debugger
npm run errorReportService
Called from npm run {demo|https|debug}
to invoke errorReportService.js
at port 8081
npm run validationService
Called from npm run {demo|https|debug}
to invoke validationService.js
at port 8082
npm run integrity-service-helpers
Build demo-backend/integrity-service-helpers/
as Node addon API
npm run validation-console
Build demo-backend/validation-console/
Validation Console GUI, which is served via validationService.js
npm run demo-certificates
Generate certificates for the demo
npm run demo-certificates -- ${hostname}
- Server certificate at demo-keys/demoCA/localhost.{crt|key}
; demo-keys/demoCA/demoCA.{crt|key}
if missingnpm run demo-certificates -- client client
- Client certificate at demo-keys/demoCA/client.{crt|key|pfx}
; demo-keys/demoCA/demoCA.{crt|key}
if missinggulp demo-certificates
taskdemo-keys/demoCA/demoCA.crt
must be trusted as a root CA by the local Chrome browser at build timecd demo-keys; certutil -d sql:$HOME/.pki/nssdb -A -n 'thin-hook demo CA' -i ./demoCA/demoCA.crt -t TCP,TCP,TCP
demo-keys/demoCA/client.pfx
must be imported as a user certificate by the browser to open Validation Consolecd demo-keys; pk12util -d sql:$HOME/.pki/nssdb -i ./demoCA/client.pfx
npm run clean-demo-certificates
Clean up certificates in demo-keys/demoCA/
. Each removal must be confirmed via rm -rvi
npm run cacheBundleUploadService
Called from npm run cache-bundle
to invoke cacheBundleUploadService.js
npm run cacheBundleGeneration
Called from npm run cache-bundle
to invoke cacheBundleGeneration.js
npm run loadOnly
Called from npm run updateHtmlHash
to invoke cacheBundleGeneration.js
in loadOnly
mode
npm run demo-frontend-modules
Install demo/node_modules
based on demo/package.json
for frontend modules for the demo. demo/package-lock.json
is updated.
npm run demo-frontend-modules-locked
Called from gulp demo-frontend-modules-locked
to install demo/node_modules
based on demo/package-lock.json
gulp.task('default',
gulp.series(
'build', // build hook.min.js
'build:test', // build test/hook.min.js
'examples', // hook examples/*
'demo' // build demo
)
);
gulp.task('examples',
gulp.series(
'script-examples', // hook non-module script examples
'module-examples', // hook module examples
'module-examples-dependencies' // save hook.parameters.moduleDependencies at examples/moduleDependencies.json
)
);
gulp.task('demo',
gulp.series(
'integrity-service-helpers', // build demo-backend/integrity-service-helpers/
'validation-console', // build demo-backend/validation-console/
'clean-gzip', // clean demo/*.gz
'get-version', // get version from the entry page demo/original-index.html
'demo-certificates', // generate certificates in demo-keys/demoCA/ if they are missing
'demo-keys', // generate key pairs and secret keys in demo-keys/keys.json
'import-maps', // generate import maps for demo at demo/modules.importmap
'browserify-commonjs', // build demo/browserify-commonjs.js
'webpack-es6-module', // build demo/webpack-es6-module.js
'webpack-commonjs', // build demo/webpack-commonjs.js
'rollup-es-modules', // build demo/rollup-module1.js and demo/rollup-es6-module.js
'policy', // configure demo/hook-callback.js
'disable-devtools', // configure demo/disable-devtools.js
'update-integrity-js', // update demo/integrity.js for the generated public keys in base64
'update-no-hook-authorization', // update demo/no-hook-authorization.js
'update-no-hook-authorization-in-html', // update hook.min.js?no-hook-authorization=* in HTMLs
'encode-demo-html', // generate demo/index.html from demo/original-index.html
'cache-bundle', // generate demo/cache-bundle.json via puppeteer
'integrity-json', // generate demo/integrity.json
'gzip', // gzip demo/cache-bundle.json and demo/integrity.json
'demo-frontend', // refresh and generate `demo-frontend/`
)
);
gulp.task('import-maps',
gulp.series(
'demo-frontend-modules-locked', // install demo/node_modules based on demo/package.json and demo/package-lock.json
'generate-import-maps', // generate import maps for demo frontend at demo/modules.importmap based on demo/node_modules/* and demo/modules-private.importmap
'embed-import-maps', // embed the generated import maps JSON into demo/bootstrap.js
)
);
gulp.task('cache-bundle',
gulp.series(
'get-version', // get version
'dummy-integrity', // generate dummy demo/integrity.json for build
'cache-bundle-automation-json', // generate dummy demo/cache-bundle.json for build
'cache-bundle-automation', // generate demo/cache-bundle.json via npm run cache-bundle
'script-hashes', // add script hashes to demo/cache-bundle.json
'script-hashes-integrity', // update integrity attributes of script-hashes.js script element in the entry page
'update-html-hash' // update "htmlHashHex" in demo-keys/keys.json via npm run updateHtmlHash
)
);