能否判断出COSDictionary 中是否包含JS脚本
时间: 2025-08-01 11:22:51 浏览: 10
<think>我们判断COSDictionary中是否包含JavaScript脚本内容,主要关注PDF中可能包含JavaScript的几种常见结构:
1. 文档级脚本(Open Action):在文档的根字典(Catalog)中,通过/OpenAction键指定。
2. 页面级脚本:在页面字典(Page)中,通过/AA(Additional Actions)键指定,例如打开页面时执行。
3. 注释(Annotation)脚本:在注释字典中,通过/AA键指定,例如鼠标事件触发。
4. 命名动作(Named Action):例如提交表单等动作可能关联JavaScript。
5. 表单字段(Form Field)脚本:表单字段的附加动作或验证脚本。
6. 文档级命名动作:在文档的/Names字典中,可能包含JavaScript动作。
具体检查点:
- 检查字典中是否包含以下键(注意大小写,PDF中键是区分大小写的,但标准键都是大写开头):
- /JS: 直接包含JavaScript代码字符串或流
- /JavaScript: 包含另一个字典,该字典中可能有/JS键
- /S (动作类型) 为/JavaScript: 例如在动作字典中,/S为/JavaScript,然后通过/JS指定代码
- /AA (附加动作): 该字典中可能包含各种事件(如/PO、/PV、/PC等)对应的动作,这些动作可能是JavaScript动作。
因此,我们需要递归遍历COSDictionary,检查以下情况:
情况1:字典中直接包含名为COSName.JS(即"/JS")的键,且其值是一个包含JavaScript代码的字符串或流。
情况2:字典中包含名为COSName.JAVA_SCRIPT(即"/JavaScript")的键,其值是一个字典,然后在该字典中继续检查/JS键。
情况3:字典中包含名为COSName.AA(即"/AA")的键,其值是一个字典(附加动作字典),该字典的每个值都是一个动作字典,我们需要检查这些动作字典的/S键是否为COSName.JAVASCRIPT(即"/JavaScript"),如果是,则其/JS键包含代码。
情况4:字典中的/S键为COSName.JAVASCRIPT,然后检查其/JS键。
注意:间接引用(COSObject)需要解析。
步骤:
1. 递归遍历字典的每个键值对。
2. 遇到间接引用(COSObject)则解析其实际对象。
3. 如果键是我们要关注的键(如/JS, /JavaScript, /AA, /S),则进行相应检查。
4. 对于/AA字典,它是一个事件名到动作字典的映射,所以需要遍历其每个值(每个值都是一个动作字典)检查。
5. 对于动作字典,检查其/S(动作类型)是否为JavaScript,如果是,则检查其/JS键。
具体实现方法:
我们可以写一个方法:`boolean hasJavaScript(COSDictionary dict)`,递归检查字典及其嵌套字典和数组。
注意避免循环引用(PDF中可能有循环引用,但COSDictionary本身没有循环引用保护,所以我们需要自己处理,比如使用一个IdentityHashSet来记录已访问的字典)。
但是考虑到性能,我们可以只检查特定结构,而不是所有嵌套结构。因为JavaScript代码通常出现在上述几种结构中。
因此,我们只对特定键进行递归:
- 当遇到/AA键时,我们检查其值(字典)中的每个动作字典。
- 当遇到/JavaScript键时,我们检查其值(字典)中的/JS键。
- 当遇到/S键且其值为/JavaScript时,我们检查同一个字典中的/JS键。
- 当遇到/JS键时,我们检查其值(字符串或流)是否非空(即存在代码)。
另外,对于数组(COSArray),我们需要遍历其元素,如果元素是字典,则递归检查。
但是,为了避免无限递归(虽然PDF中一般不会出现太深的循环,但可能有循环引用),我们使用一个HashSet来记录已经访问过的字典(通过对象标识)。
实现步骤:
1. 创建一个静态方法:`public static boolean containsJavaScript(COSDictionary dict, Set<COSDictionary> visited)`。
2. 在方法开始,先检查visited中是否已经包含当前dict,如果包含则返回false(避免循环)。
3. 将当前dict加入visited。
4. 遍历dict的每一个键值对:
- 如果键是`COSName.JS`(即"/JS"):
- 获取值,如果是间接引用则解析。
- 如果值是COSString或COSStream,则将其内容提取为字符串(注意编码),然后判断该字符串是否非空(或者根据需求判断是否为JavaScript代码,但通常只要存在就认为有风险)。
- 注意:COSStream包含二进制数据,我们需要读取其字节流并尝试转换为字符串(使用PDFBox的IOUtils或类似工具)。
- 如果提取的字符串长度>0,则返回true。
- 如果键是`COSName.JAVA_SCRIPT`(即"/JavaScript"):
- 获取值,如果是间接引用则解析。
- 如果值是COSDictionary,则递归调用containsJavaScript检查这个子字典(传入visited集合)。
- 如果键是`COSName.AA`(即"/AA"):
- 获取值,如果是间接引用则解析。
- 如果值是COSDictionary,则遍历这个附加动作字典的每个值(每个值都是一个动作字典):
- 对每个动作字典,递归调用containsJavaScript(传入visited集合)。
- 如果值是COSArray,则遍历数组中的每个元素(可能是字典),对每个字典递归检查。
- 如果键是`COSName.S`(即"/S"):
- 获取值,如果是间接引用则解析。
- 如果值是COSName,并且等于`COSName.JAVASCRIPT`(即"/JavaScript"),那么我们需要检查当前字典(dict)中是否有/JS键,并且其值包含代码。
- 注意:这个检查不能只依赖当前键值对,因为/S键和/JS键在同一个字典中。所以我们在同一个字典中检查完/S键后,可以立即检查同字典的/JS键吗?但是遍历顺序可能先遇到/S键,然后才遇到/JS键。所以我们可以这样:在同一个字典中,如果遇到/S键且其值为/JavaScript,那么我们就检查这个字典中是否包含/JS键(不管遍历到没有),我们可以直接在这个字典中查找/JS键。
- 因此,在这种情况下,我们可以直接在当前字典中查找名为`COSName.JS`的键,然后检查其值(字符串或流)是否包含代码。
- 注意:其他键我们暂时不关心,但是值如果是字典或数组,我们也要递归检查吗?为了安全起见,我们应该递归检查所有嵌套的字典和数组,因为JavaScript代码可能被隐藏在其他结构中。但是这样可能会增加计算量。因此,我们可以根据需求选择:如果只检查上述明确的结构,那么就不递归其他键;如果希望全面检查,则递归所有字典和数组。
考虑到性能,我们可以只递归上述提到的特定结构(/AA, /JavaScript, 以及数组中的字典元素),而忽略其他键的嵌套结构。但是,如果JavaScript代码被放在一个非常深的结构中,我们可能会漏掉。所以,为了安全,我们选择递归所有嵌套的字典和数组,但要注意循环引用。
5. 对于数组(COSArray):
- 遍历数组中的每个元素。
- 如果元素是间接引用,则解析。
- 如果元素是COSDictionary,则递归调用containsJavaScript。
- 如果元素是COSArray,则递归检查数组(可以写一个检查数组的方法,或者递归调用同一个方法,但方法需要支持数组参数?这里我们重载一个方法用于数组)。
6. 但是,由于我们是从字典开始的,所以我们在遍历字典的值时,如果值是数组,则递归检查数组;如果值是字典,则递归检查字典。
因此,我们重构:写一个检查COSBase的方法,然后由检查字典和数组的方法调用它。
然而,为了简化,我们可以这样:
在遍历字典的每个值时,如果值是字典(包括间接引用解析后),则递归检查这个字典;如果值是数组,则递归检查数组(遍历数组的每个元素,如果是字典则递归检查)。
但是,注意:在检查/AA键时,它的值可能是一个数组(在PDF规范中,/AA的值是字典,但有些可能不规范?),所以我们在处理/AA键时已经做了处理。对于其他键,我们也要这样处理吗?
为了统一,我们可以在遍历字典的每个键值对时,对值进行如下处理:
- 如果值是间接引用,则解析。
- 如果值是字典,则递归检查这个字典。
- 如果值是数组,则遍历数组的每个元素,对每个元素:
- 如果是间接引用则解析
- 如果是字典,则递归检查
- 如果是数组,则递归遍历(这种情况较少,但数组嵌套是允许的)
这样,我们就递归检查了所有嵌套的字典。
但是,这样会遍历所有嵌套结构,可能会比较慢。我们可以优化:在检查到特定键(如/JS)时,我们才进行深度检查,否则只检查直接键。但这样可能会漏掉深层嵌套的JS代码。所以,为了安全,我们选择全面递归。
然而,全面递归可能会遇到性能问题,特别是对于非常大的PDF。因此,我们可以设置一个递归深度限制,或者由调用者控制是否深度检查。
但是,题目要求是判断是否包含JavaScript,安全起见,我们应该全面递归。
实现方案:
我们将编写一个递归方法,可以处理字典和数组(作为参数)。但是,为了统一,我们可以将数组转换为一组对象,然后对每个对象如果是字典就递归。对于非字典和非数组的对象,我们忽略。
具体步骤:
方法1:`checkObjectForJavaScript(COSBase base, Set<COSDictionary> visitedDicts, Set<COSArray> visitedArrays)`
- base:要检查的对象
- visitedDicts:已访问的字典集合(防止循环)
- visitedArrays:已访问的数组集合(防止循环,虽然数组循环较少见)
但是,这样比较复杂。由于PDF中循环引用通常发生在字典之间,数组循环较少,我们可以只记录字典的循环。
因此,我们只用一个visited集合(存储COSDictionary),然后:
- 如果base是COSDictionary,则用之前的方法检查这个字典(并加入visited)。
- 如果base是COSArray,则遍历数组中的每个元素,对每个元素递归调用checkObjectForJavaScript(传入visited,但注意数组元素可能是字典,也可能是数组,所以递归下去)。
但是,为了避免数组的重复访问,我们也可以记录数组,但为了简单,我们只防止字典循环,因为数组嵌套通常不会太深。
因此,我们设计两个方法:
方法1:`public static boolean containsJavaScript(COSDictionary dict, Set<COSDictionary> visited)`
方法2:`public static boolean containsJavaScript(COSArray array, Set<COSDictionary> visited)`
然后,在检查字典时,如果遇到数组,则调用方法2;在检查数组时,如果遇到字典,则调用方法1,遇到数组则递归。
但是,这样代码重复。我们可以写一个方法`checkBase(COSBase base, Set<COSDictionary> visited)`,根据base的类型分派。
具体实现:
由于时间关系,我们采用一个统一的方法,并递归检查。
伪代码:
方法:`boolean hasJavaScriptContent(COSBase base, Set<COSDictionary> visited)`
步骤:
if (base == null) return false;
if (base instanceof COSObject) {
base = ((COSObject) base).getObject();
// 继续检查base
}
if (base instanceof COSDictionary) {
COSDictionary dict = (COSDictionary) base;
if (visited.contains(dict)) {
return false;
}
visited.add(dict);
// 检查这个字典本身是否包含JavaScript
// 1. 检查/JS键
COSBase jsValue = dict.getDictionaryObject(COSName.JS);
if (jsValue != null) {
if (jsValue instanceof COSObject) {
jsValue = ((COSObject) jsValue).getObject();
}
// 如果/JS键的值是字符串或流,则提取内容并判断是否非空
if (jsValue instanceof COSString) {
String jsCode = ((COSString) jsValue).getString();
if (!jsCode.isEmpty()) {
return true;
}
} else if (jsValue instanceof COSStream) {
// 读取流内容
try (InputStream in = ((COSStream) jsValue).createInputStream()) {
String jsCode = IOUtils.toString(in, StandardCharsets.ISO_8859_1);
if (!jsCode.isEmpty()) {
return true;
}
} catch (IOException e) {
// 忽略,可能是空流
}
}
}
// 2. 检查/JavaScript键
COSBase javascriptValue = dict.getDictionaryObject(COSName.JAVA_SCRIPT);
if (javascriptValue != null) {
// 递归检查这个值(可能是字典)
if (hasJavaScriptContent(javascriptValue, visited)) {
return true;
}
}
// 3. 检查/AA键
COSBase aaValue = dict.getDictionaryObject(COSName.AA);
if (aaValue != null) {
if (hasJavaScriptContent(aaValue, visited)) {
return true;
}
}
// 4. 检查/S键是否为JavaScript
COSBase sValue = dict.getDictionaryObject(COSName.S);
if (sValue instanceof COSName &&
((COSName)sValue).equals(COSName.JAVASCRIPT)) {
// 检查同一个字典中的/JS键
if (jsValue != null) { // 上面已经检查过/JS键,如果非空且包含代码,上面已经返回true
// 所以这里不需要再检查,但为了逻辑清晰,我们也可以再检查一遍?或者上面检查/JS键时已经返回,所以这里不会执行
// 因此,如果执行到这里,说明有/S为JavaScript,但/JS键没有内容(或者为空),那么可能是安全的,所以继续。
} else {
// 但是,有可能/JS键在字典中,但之前没有解析(因为jsValue是通过getDictionaryObject获取的,可能为null),所以这里我们重新获取?
// 实际上,jsValue就是上面获取的,所以如果为null,说明没有/JS键。所以不需要再检查。
}
// 注意:还有一种情况,/JS键可能是一个间接引用,但上面已经解析了。所以这里不需要再处理。
}
// 5. 遍历字典的所有值,递归检查(除了上面已经检查过的特定键?但为了全面,我们还是要递归检查所有值,因为可能有嵌套的字典或数组包含JS)
// 但是,上面已经检查了/AA和/JavaScript,所以这里我们要跳过吗?不,因为可能有其他键包含嵌套的JS,所以我们要全部遍历。
for (Map.Entry<COSName, COSBase> entry : dict.entrySet()) {
COSName key = entry.getKey();
COSBase value = entry.getValue();
// 跳过已经检查过的键(可选项,但跳过不影响,因为递归到同一个字典会被visited阻止)
// 但是,我们上面已经检查了/JS, /JavaScript, /AA, /S,所以可以跳过这些键吗?
// 不行,因为/AA的值是一个字典,这个字典里可能还有嵌套的/AA?所以还是要递归。
// 因此,我们不跳过,但为了避免重复检查,visited会阻止重复字典。
// 递归检查值
if (hasJavaScriptContent(value, visited)) {
return true;
}
}
} else if (base instanceof COSArray) {
COSArray array = (COSArray) base;
for (int i=0; i<array.size(); i++) {
COSBase element = array.get(i);
if (hasJavaScriptContent(element, visited)) {
return true;
}
}
}
return false;
但是,上面的步骤中,在检查字典的/JS键时,我们只检查了当前字典的/JS键,没有递归检查/JS键的值(因为/JS键的值可能是字典?但规范中/JS键的值应该是字符串或流,所以不需要递归?)。但是,为了安全,如果/JS键的值是一个字典,我们也应该递归检查吗?规范中不是,但可能有人恶意构造?所以,我们可以递归检查/JS键的值(通过hasJavaScriptContent方法),这样如果值是字典或数组,也会被检查。
但是,我们上面在检查/JS键时,如果是字符串或流,我们判断内容非空就返回true。如果是字典或数组,我们也会进入递归检查。所以,我们可以将/JS键的值也交给递归方法处理。
因此,我们可以简化:在遍历字典的所有键值对之前,先检查特定键(/JS, /JavaScript, /AA)并递归,然后遍历所有值(包括这些键的值)?这样就会重复检查。所以,我们可以在递归检查特定键的值后,再在遍历所有值的时候跳过这些键?这样比较麻烦。
另一种思路:我们不在前面单独检查这些键,而是在遍历字典的每个键值对时,对每个值都递归调用hasJavaScriptContent,这样这些键的值自然会被递归。但是,这样我们就无法在检查/JS键时直接判断字符串内容(因为递归到/JS键的值(字符串)时,字符串不是字典也不是数组,所以不会进入递归,也就不会触发检查)。
所以,我们需要在遇到字符串和流时,在hasJavaScriptContent方法中判断吗?不,因为字符串和流只有在作为/JS键的值时才表示JavaScript代码,在其他地方可能只是普通字符串。
因此,我们必须结合上下文(键名)来判断。所以,我们必须在检查字典时,对特定键(/JS)的值进行特殊处理(即判断字符串内容),而其他键的值如果是字符串,则忽略。
所以,我们上面单独检查/JS键的方法是必要的。
那么,我们如何避免重复检查?比如,在单独检查/JS键时,我们递归检查了它的值(通过hasJavaScriptContent,如果值是字典或数组,会递归),然后在后面的遍历中,我们又会再次检查这个值(因为我们在遍历字典的所有值)。为了避免重复,我们可以不单独检查,而是修改遍历过程:在遍历字典的键值对时,如果遇到/JS键,则进行特殊处理(检查字符串或流),否则递归检查值。
因此,修改方案:
在遍历字典的每个键值对时:
- 如果键是/JS,则特殊处理(检查值是否为字符串或流,并判断内容是否非空)
- 否则,递归检查值(即调用hasJavaScriptContent(value, visited))
但是,这样/JavaScript键和/AA键就没有被特殊处理(即没有在遇到键名时就处理,而是当作普通键递归检查值)。所以,我们需要在递归检查值之前,对特定键进行特殊处理。
所以,我们可以在遍历键值对时,这样写:
for (Map.Entry<COSName, COSBase> entry : dict.entrySet()) {
COSName key = entry.getKey();
COSBase value = entry.getValue();
// 处理间接引用
if (value instanceof COSObject) {
value = ((COSObject) value).getObject();
// 更新entry中的value?不行,因为entry是只读的,所以我们要用value
}
if (key.equals(COSName.JS)) {
// 检查value
if (value instanceof COSString) {
String jsCode = ((COSString) value).getString();
if (!jsCode.isEmpty()) {
return true;
}
} else if (value instanceof COSStream) {
try (InputStream in = ((COSStream) value).createInputStream()) {
String jsCode = IOUtils.toString(in, StandardCharsets.ISO_8859_1);
if (!jsCode.isEmpty()) {
return true;
}
} catch (IOException e) {
// 忽略
}
} else {
// 如果不是字符串或流,递归检查(可能是字典或数组)
if (hasJavaScriptContent(value, visited)) {
return true;
}
}
} else if (key.equals(COSName.S)) {
// 如果键是/S,并且值是/COSName.JAVASCRIPT,那么我们需要检查同一个字典中的/JS键
if (value instanceof COSName && ((COSName)value).equals(COSName.JAVASCRIPT)) {
// 获取当前字典中的/JS键的值
COSBase jsValue = dict.getDictionaryObject(COSName.JS);
if (jsValue != null) {
// 递归检查这个/JS值(可能是字符串、流、字典等)
if (hasJavaScriptContent(jsValue, visited)) {
return true;
}
}
}
} else if (key.equals(COSName.JAVA_SCRIPT) || key.equals(COSName.AA)) {
// 对于/JavaScript和/AA键,我们直接递归检查其值
if (hasJavaScriptContent(value, visited)) {
return true;
}
} else {
// 其他键,递归检查值
if (hasJavaScriptContent(value, visited)) {
return true;
}
}
}
同时,在遍历字典之前,先加入visited集合。
另外,对于/S键的处理,我们只处理了当/S键的值是JavaScript时,去检查/JS键。但是,/JS键可能还没有被遍历到(因为遍历顺序),所以我们在/S键的处理中,直接去字典中获取/JS键,这样即使还没有遍历到/JS键,也能检查到。
但是,注意:如果/JS键是一个间接引用,那么dict.getDictionaryObject(COSName.JS)会解析间接引用吗?在PDFBox中,getDictionaryObject方法会自动解析间接引用,所以返回的是直接对象。
因此,我们按照这个方案实现。
但是,这样会有一个问题:在检查/S键时,我们递归检查了/JS键的值,而在遍历到/JS键时,我们还会再次检查。这样重复检查了。为了避免重复,我们可以不在/S键的处理中递归检查/JS键,而是等到遍历到/JS键时再检查。但是,这样如果/JS键在/S键之前遍历到,那么就会在/JS键处返回true;如果/JS键在/S键之后,那么我们在/S键处提前检查,然后返回true。这样不会漏掉,但可能会重复检查(同一个/JS键被检查两次)。但是,由于/JS键的值是字符串或流,在检查/S键时,我们调用hasJavaScriptContent(jsValue, visited),在字符串的情况下,我们不会进入递归(因为字符串不是字典也不是数组),所以只是判断了字符串非空,然后返回。在遍历到/JS键时,我们还会再判断一次。所以,为了避免重复,我们可以在/S键的处理中,不通过递归,而是直接判断/JS键的值是否为字符串或流,并检查内容。
因此,修改/S键的处理:
else if (key.equals(COSName.S)) {
if (value instanceof COSName && ((COSName)value).equals(COSName.JAVASCRIPT)) {
COSBase jsValue = dict.getDictionaryObject(COSName.JS);
if (jsValue != null) {
if (jsValue instanceof COSObject) {
jsValue = ((COSObject) jsValue).getObject();
}
if (jsValue instanceof COSString) {
String jsCode = ((COSString) jsValue).getString();
if (!jsCode.isEmpty()) {
return true;
}
} else if (jsValue instanceof COSStream) {
try (InputStream in = ((COSStream) jsValue).createInputStream()) {
String jsCode = IOUtils.toString(in, StandardCharsets.ISO_8859_1);
if (!jsCode.isEmpty()) {
return true;
}
} catch (IOException e) {
// 忽略
}
} else {
// 如果是其他类型,递归检查(虽然规范不是,但可能嵌套)
if (hasJavaScriptContent(jsValue, visited)) {
return true;
}
}
}
}
}
这样,在/S键的处理中,我们直接检查/JS键的值(字符串或流),和/JS键的处理逻辑一样。
但是,这样代码重复。我们可以提取一个方法:
private static boolean checkJSValue(COSBase base) {
if (base == null) return false;
if (base instanceof COSObject) {
base = ((COSObject) base).getObject();
}
if (base instanceof COSString) {
return !((COSString) base).getString().isEmpty();
} else if (base instanceof COSStream) {
try (InputStream in = ((COSStream) base).createInputStream()) {
String jsCode = IOUtils.toString(in, StandardCharsets.ISO_8859_1);
return !jsCode.isEmpty();
} catch (IOException e) {
return false;
}
}
return false;
}
然后,在/JS键的处理和/S键的处理中,都可以调用这个方法。
但是,注意:在/JS键的处理中,我们除了检查字符串和流,还递归检查了其他类型(字典、数组),所以不能完全用这个方法替代。
因此,我们保留递归检查,但在/JS键的处理中,如果是字符串或流,我们用这个方法判断,否则递归。
所以,重构:
private static boolean checkJSValue(COSBase base, Set<COSDictionary> visited) {
if (base == null) {
return false;
}
if (base instanceof COSObject) {
base = ((COSObject) base).getObject();
}
if (base instanceof COSString) {
return !((COSString) base).getString().isEmpty();
} else if (base instanceof COSStream) {
try (InputStream in = ((COSStream) base).createInputStream()) {
String jsCode = IOUtils.toString(in, StandardCharsets.ISO_8859_1);
return !jsCode.isEmpty();
} catch (IOException e) {
return false;
}
} else {
// 递归检查嵌套结构
return hasJavaScriptContent(base, visited);
}
}
然后,在/JS键和/S键(处理/JS键的值)时,调用这个方法。
但是,注意:在/S键的处理中,我们调用checkJSValue(jsValue, visited),如果返回true,则返回true。
在/JS键的处理中,我们调用checkJSValue(value, visited),如果返回true,则返回true。
这样,代码可以复用。
最终代码结构:
public static boolean hasJavaScriptContent(COSBase base, Set<COSDictionary> visited) {
if (base == null) {
return false;
}
// 处理间接引用
if (base instanceof COSObject) {
base = ((COSObject) base).getObject();
}
if (base instanceof COSDictionary) {
COSDictionary dict = (COSDictionary) base;
if (visited.contains(dict)) {
return false;
}
visited.add(dict);
for (Map.Entry<COSName, COSBase> entry : dict.entrySet()) {
COSName key = entry.getKey();
COSBase value = entry.getValue();
if (key.equals(COSName.JS)) {
if (checkJSValue(value, visited)) {
return true;
}
} else if (key.equals(COSName.S)) {
if (value instanceof COSName && ((COSName)value).equals(COSName.JAVASCRIPT)) {
COSBase jsValue = dict.getDictionaryObject(COSName.JS);
if (jsValue != null) {
if (checkJSValue(jsValue, visited)) {
return true;
}
}
}
} else if (key.equals(COSName.JAVA_SCRIPT) || key.equals(COSName.AA)) {
// 递归检查
if (hasJavaScriptContent(value, visited)) {
return true;
}
} else {
// 其他键,递归检查值
if (hasJavaScriptContent(value, visited)) {
return true;
}
}
}
} else if (base instanceof COSArray) {
COSArray array = (COSArray) base;
for (int i=0; i<array.size(); i++) {
COSBase element = array.get(i);
if (hasJavaScriptContent(element, visited)) {
return true;
}
}
}
return false;
}
private static boolean checkJSValue(COSBase base, Set<COSDictionary> visited) {
if (base == null) {
return false;
}
if (base instanceof COSObject) {
base = ((COSObject) base).getObject();
}
if (base instanceof COSString) {
return !((COSString) base).getString().isEmpty();
} else if (base instanceof COSStream) {
try (InputStream in = ((COSStream) base).createInputStream()) {
String jsCode = IOUtils.toString(in, StandardCharsets.ISO_8859_1);
return !jsCode.isEmpty();
} catch (IOException e) {
return false;
}
} else {
// 递归检查嵌套结构
return hasJavaScriptContent(base, visited);
}
}
注意:上面的方法在检查字典时,对于/JavaScript和/AA键,我们直接递归检查其值,而其他键则递归检查值(包括嵌套)。这样就能覆盖所有情况。
但是,有一个问题:在/AA键的值中,规范是一个字典(事件名->动作字典),所以我们在/AA键的处理中递归检查,这个递归会进入字典,然后检查字典中的每一个键值对(包括/S键和/JS键等)。所以,这样设计是合理的。
另外,为了避免重复检查同一个字典,visited集合会记录。
最后,我们可以提供一个公共入口方法,不需要传入visited:
public static boolean containsJavaScript(COSDictionary dict) {
return hasJavaScriptContent(dict, new IdentityHashSet<>());
}
注意:使用IdentityHashSet(通过System.identityHashCode)来比较字典对象,避免循环引用。
由于PDFBox的COSDictionary没有重写equals和hashCode,所以我们可以用IdentityHashMap模拟一个IdentityHashSet,或者使用Collections.newSetFromMap(new IdentityHashMap<>())。
示例:
Set<COSDictionary> visited = Collections.newSetFromMap(new IdentityHashMap<>());
这样,即使两个字典内容相同,但对象不同,也会被视为不同。
完整代码示例:
注意:需要PDFBox和IOUtils(Apache Commons IO)的依赖。
由于代码较长,这里只给出核心方法。在实际应用中,还需要考虑异常处理等。
但是,注意:在读取流时,我们使用了ISO_8859_1编码,因为PDF中的字符串可能是二进制,但JavaScript代码通常是ASCII,所以用ISO_8859_1可以保留原始字节。
另外,如果流很大,读取整个流可能会消耗内存,所以我们可以只读取一部分?但为了判断是否为空,我们只需要读取第一个字节即可。但是,为了安全,我们读取整个流,因为可能只有一字节的空字符?但通常不会,所以我们可以判断流的大小,如果大小为0,则直接返回false。
优化:在检查流时,可以先获取流长度,如果长度为0,则跳过读取。
if (base instanceof COSStream) {
COSStream stream = (COSStream) base;
if (stream.getLength() == 0) {
return false;
}
// 否则读取内容
...
}
但是,PDF中的流长度可能通过/Length指定,但实际内容可能更多(有endstream标记),所以使用getLength()是安全的。
因此,修改checkJSValue中关于COSStream的部分:
} else if (base instanceof COSStream) {
COSStream stream = (COSStream) base;
long length = stream.getLength();
if (length == 0) {
return false;
}
try (InputStream in = stream.createInputStream()) {
// 读取内容
String jsCode = IOUtils.toString(in, StandardCharsets.ISO_8859_1);
return !jsCode.isEmpty();
} catch (IOException e) {
return false;
}
}
这样,空流就不会读取。
最后,我们提供一个完整的工具类。
由于代码较长,这里只给出核心部分。
使用示例:
COSDictionary dict = ...; // 要检查的字典
boolean hasJS = PDFJavaScriptDetector.containsJavaScript(dict);
注意:这个方法可能会递归遍历整个字典结构,所以对于非常大的PDF,可能会慢。在实际应用中,可以考虑异步执行或设置超时。
另外,如果PDF中包含循环引用,visited集合可以防止无限递归。
至此,我们完成了判断COSDictionary中是否包含JavaScript的方法。</think>要判断`COSDictionary`对象中是否包含JavaScript脚本内容,可以通过检查特定键和值类型来实现。以下是完整的解决方案:
```java
import org.apache.pdfbox.cos.*;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageTree;
import org.apache.pdfbox.util.IOUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Set;
public class PDFJavaScriptDetector {
public static void main(String[] args) {
try (PDDocument document = PDDocument.load(new File("example.pdf"))) {
// 检查整个文档是否包含JavaScript
boolean hasJS = containsJavaScript(document.getDocumentCatalog().getCOSObject());
System.out.println("文档包含JavaScript: " + hasJS);
// 检查所有页面
PDPageTree pages = document.getPages();
for (PDPage page : pages) {
boolean pageHasJS = containsJavaScript(page.getCOSObject());
System.out.println("页面包含JavaScript: " + pageHasJS);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 检查COSDictionary是否包含JavaScript
* @param dict 要检查的字典
* @return 是否包含JavaScript内容
*/
public static boolean containsJavaScript(COSDictionary dict) {
return hasJavaScriptContent(
dict,
Collections.newSetFromMap(new IdentityHashMap<>())
);
}
private static boolean hasJavaScriptContent(COSBase base, Set<COSDictionary> visited) {
if (base == null) return false;
// 处理间接引用
if (base instanceof COSObject) {
base = ((COSObject) base).getObject();
}
// 检查字典
if (base instanceof COSDictionary) {
COSDictionary dict = (COSDictionary) base;
// 防止循环引用
if (visited.contains(dict)) return false;
visited.add(dict);
// 检查所有键值对
for (Map.Entry<COSName, COSBase> entry : dict.entrySet()) {
COSName key = entry.getKey();
COSBase value = entry.getValue();
// 1. 检查/JS键
if (COSName.JS.equals(key)) {
if (isJSCode(value)) {
return true;
}
}
// 2. 检查/JavaScript键
else if (COSName.JAVA_SCRIPT.equals(key)) {
if (hasJavaScriptContent(value, visited)) {
return true;
}
}
// 3. 检查/AA(附加动作)键
else if (COSName.AA.equals(key)) {
if (hasJavaScriptContent(value, visited)) {
return true;
}
}
// 4. 检查/S(动作类型)键
else if (COSName.S.equals(key) &&
value instanceof COSName &&
COSName.JAVASCRIPT.equals(value)) {
// 检查同字典中的/JS键
COSBase jsValue = dict.getDictionaryObject(COSName.JS);
if (isJSCode(jsValue)) {
return true;
}
}
// 递归检查其他值
else if (hasJavaScriptContent(value, visited)) {
return true;
}
}
}
// 检查数组
else if (base instanceof COSArray) {
COSArray array = (COSArray) base;
for (int i = 0; i < array.size(); i++) {
if (hasJavaScriptContent(array.get(i), visited)) {
return true;
}
}
}
return false;
}
/**
* 检查对象是否包含JavaScript代码
*/
private static boolean isJSCode(COSBase base) {
if (base == null) return false;
// 处理间接引用
if (base instanceof COSObject) {
base = ((COSObject) base).getObject();
}
try {
// 字符串类型的JavaScript代码
if (base instanceof COSString) {
return !((COSString) base).getString().trim().isEmpty();
}
// 流类型的JavaScript代码
else if (base instanceof COSStream) {
try (InputStream in = ((COSStream) base).createInputStream()) {
String content = IOUtils.toString(in, StandardCharsets.ISO_8859_1);
return !content.trim().isEmpty();
}
}
} catch (IOException e) {
// 处理异常
}
return false;
}
}
```
### 关键检查点说明
1. **/JS键检查**:
- 直接包含JavaScript代码的键
- 值可以是`COSString`(文本)或`COSStream`(二进制流)
- 检查内容是否非空字符串
2. **/JavaScript键检查**:
- 通常包含嵌套的JavaScript字典
- 递归检查嵌套字典中的内容
3. **/AA键检查(附加动作)**:
```java
if (COSName.AA.equals(key)) {
if (hasJavaScriptContent(value, visited)) {
return true;
}
}
```
- 包含页面打开/关闭等事件的脚本
- 递归检查嵌套的动作字典[^1]
4. **/S键检查(动作类型)**:
```java
else if (COSName.S.equals(key) &&
value instanceof COSName &&
COSName.JAVASCRIPT.equals(value)) {
// 检查同字典中的/JS键
COSBase jsValue = dict.getDictionaryObject(COSName.JS);
if (isJSCode(jsValue)) {
return true;
}
}
```
- 当动作类型为JavaScript时
- 检查同字典中的/JS键是否包含代码
5. **递归检查**:
- 处理嵌套字典和数组结构
- 使用`IdentityHashMap`防止循环引用
### JavaScript在PDF中的常见位置
1. **文档级脚本**:
- 存储在根字典`/OpenAction`或`/Names`中
2. **页面级脚本**:
- 存储在页面字典的`/AA`键中
3. **注释脚本**:
- 表单字段或注释的附加动作
4. **表单验证脚本**:
- 表单字段的`/A`或`/AA`键中
### 注意事项
1. **编码处理**:
- JavaScript代码可能使用`PDFDocEncoding`或`UTF-16BE`
- 使用`StandardCharsets.ISO_8859_1`确保原始字节正确读取
2. **性能优化**:
- 大型PDF使用深度限制防止栈溢出
```java
// 添加深度参数
hasJavaScriptContent(base, visited, depth+1)
```
3. **安全考虑**:
- 恶意PDF可能包含多层嵌套结构
- 设置最大递归深度(建议不超过20层)
4. **流处理**:
- 使用try-with-resources确保流正确关闭
- 检查流长度避免读取大文件
阅读全文
相关推荐














