@sun-asterisk/sunlint
Version:
☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards
946 lines (826 loc) • 31.6 kB
JavaScript
/**
* ESLint rule: S002 – IDOR Check
* Rule ID: custom/s002
* Description: Verify IDOR (Insecure Direct Object Reference) security in REST APIs.
*/
"use strict";
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Verify IDOR (Insecure Direct Object Reference) security in REST APIs',
recommended: true,
url: 'https://owasp.org/www-project-top-ten/2017/A5_2017-Broken_Access_Control'
},
schema: [],
messages: {
restEndpointMissingAuth: 'IDOR Risk: REST endpoint with ID parameter lacks authorization check. Add proper authentication middleware or manual ownership validation.',
repositoryMissingConstraint: 'IDOR Risk: Database query should include user/owner constraint (e.g., WHERE user_id = $userId) to prevent unauthorized data access.',
directEntityReturn: 'IDOR Risk: Avoid returning Entity/Model directly from REST endpoints. Use DTO/ResponseModel to control data exposure.',
sequentialIdUsage: 'IDOR Risk: Consider using UUID instead of sequential ID (number) to make resource identifiers unpredictable.',
directRepositoryAccess: 'IDOR Risk: Direct repository/database access with ID from URL without authorization check. Verify user ownership before accessing resources.',
},
},
create(context) {
return {
// Check REST endpoints (Express.js, NestJS, etc.)
CallExpression(node) {
checkRestEndpointAuthorization(node, context);
checkDirectRepositoryAccess(node, context);
},
// Check method definitions
MethodDefinition(node) {
checkRepositorySecurityConstraint(node, context);
checkDirectEntityReturn(node, context);
},
// Check function declarations
FunctionDeclaration(node) {
checkRepositorySecurityConstraint(node, context);
checkDirectEntityReturn(node, context);
},
// Check variable declarations for ID fields
VariableDeclarator(node) {
checkSequentialIdUsage(node, context);
},
// Check property definitions (for class properties)
PropertyDefinition(node) {
checkSequentialIdUsage(node, context);
},
// Check arrow function expressions
ArrowFunctionExpression(node) {
checkRepositorySecurityConstraint(node, context);
checkDirectEntityReturn(node, context);
},
// Check function expressions
FunctionExpression(node) {
checkRepositorySecurityConstraint(node, context);
checkDirectEntityReturn(node, context);
},
};
// =================== CHECK METHODS ===================
/**
* Check if the endpoint has security middleware
*/
function hasSecurityMiddleware(node) {
if (node.type !== 'CallExpression') return false;
// Check for middleware functions like auth, authenticate, authorize
return node.arguments.some(arg => {
if (arg.type === 'Identifier') {
const argName = arg.name.toLowerCase();
return argName.includes('auth') ||
argName.includes('guard') ||
argName.includes('protect') ||
argName.includes('secure') ||
argName.includes('jwt') ||
argName.includes('token');
}
return false;
});
}
/**
* Check if the function has manual security check
*/
function hasManualSecurityCheck(node) {
let bodyNode;
if (node.type === 'CallExpression') {
// Check callback function in Express.js
const callback = node.arguments.find(arg =>
arg.type === 'FunctionExpression' ||
arg.type === 'ArrowFunctionExpression'
);
if (callback && callback.body) {
bodyNode = callback.body;
}
} else if (node.body) {
bodyNode = node.body;
}
if (!bodyNode) return false;
// If arrow function with expression body
if (bodyNode.type !== 'BlockStatement') {
const bodyText = context.getSourceCode().getText(bodyNode).toLowerCase();
return containsSecurityCheck(bodyText);
}
// Check block statement
return bodyNode.body.some(statement => {
const statementText = context.getSourceCode().getText(statement).toLowerCase();
return containsSecurityCheck(statementText);
});
}
/**
* Check REST endpoint with ID parameter but missing authorization
*/
function checkRestEndpointAuthorization(node, context) {
if (!isRestEndpoint(node)) return;
const hasIdParameter = hasPathParameterId(node);
const hasSecurityMiddlewareResult = hasSecurityMiddleware(node);
const hasManualSecurityCheckResult = hasManualSecurityCheck(node);
if (hasIdParameter && !hasSecurityMiddlewareResult && !hasManualSecurityCheckResult) {
context.report({
node,
messageId: 'restEndpointMissingAuth',
});
}
}
/**
* Check if the method has custom query
*/
function hasCustomQuery(node) {
if (!node.body || node.body.type !== 'BlockStatement') return false;
return node.body.body.some(statement => {
const statementText = context.getSourceCode().getText(statement).toLowerCase();
return statementText.includes('query') ||
statementText.includes('sql') ||
statementText.includes('select') ||
statementText.includes('from') ||
statementText.includes('where') ||
statementText.includes('findby') ||
statementText.includes('createquerybuilder') ||
statementText.includes('rawquery');
});
}
/**
* Check if query contains user constraint
*/
function hasUserConstraintInQuery(node) {
if (!node.body || node.body.type !== 'BlockStatement') return false;
return node.body.body.some(statement => {
const statementText = context.getSourceCode().getText(statement).toLowerCase();
return statementText.includes('user_id') ||
statementText.includes('owner_id') ||
statementText.includes('created_by') ||
statementText.includes('userid') ||
statementText.includes('ownerid') ||
statementText.includes('createdby') ||
(statementText.includes('where') &&
(statementText.includes('user') || statementText.includes('owner')));
});
}
/**
* Check Repository query missing user/owner constraint
*/
function checkRepositorySecurityConstraint(node, context) {
if (!isRepositoryMethod(node)) return;
const hasCustomQueryResult = hasCustomQuery(node);
const hasUserConstraint = hasUserConstraintInQuery(node);
if (hasCustomQueryResult && !hasUserConstraint) {
context.report({
node,
messageId: 'repositoryMissingConstraint',
});
}
}
/**
* Check if method returns Entity directly instead of DTO
*/
function checkDirectEntityReturn(node, context) {
if (!isRestEndpoint(node)) return;
const returnsEntity = returnsEntityDirectly(node);
if (returnsEntity) {
context.report({
node,
messageId: 'directEntityReturn',
});
}
}
/**
* Check if ID field uses sequential number
*/
function checkSequentialIdUsage(node, context) {
const isEntityId = isEntityIdField(node);
const isSequentialType = isSequentialIdType(node);
if (isEntityId && isSequentialType) {
context.report({
node,
messageId: 'sequentialIdUsage',
});
}
}
/**
* Check direct repository access without authorization check
*/
function checkDirectRepositoryAccess(node, context) {
if (!isRepositoryFindMethod(node)) return;
const enclosingFunction = getEnclosingFunction(node);
if (!enclosingFunction) return;
const isInRestControllerResult = isInRestController(enclosingFunction);
const hasIdParameter = hasPathParameterId(enclosingFunction);
const hasAuthCheck = hasAuthorizationLogicNearby(node, enclosingFunction);
if (isInRestControllerResult && hasIdParameter && !hasAuthCheck) {
context.report({
node,
messageId: 'directRepositoryAccess',
});
}
}
// =================== HELPER METHODS ===================
/**
* Check if the call expression is a REST endpoint
*/
function isRestEndpoint(node) {
// Express.js style: app.get(), router.post(), etc.
if (node.type === 'CallExpression') {
const callee = node.callee;
if (callee.type === 'MemberExpression') {
const property = callee.property;
if (property.type === 'Identifier') {
const methodName = property.name;
return ['get', 'post', 'put', 'delete', 'patch', 'use', 'all'].includes(methodName);
}
}
}
// NestJS style: @Get(), @Post(), etc.
if (node.decorators) {
return node.decorators.some(decorator => {
if (decorator.expression.type === 'Identifier') {
const decoratorName = decorator.expression.name;
return ['Get', 'Post', 'Put', 'Delete', 'Patch', 'All'].includes(decoratorName);
}
if (decorator.expression.type === 'CallExpression' &&
decorator.expression.callee.type === 'Identifier') {
const decoratorName = decorator.expression.callee.name;
return ['Get', 'Post', 'Put', 'Delete', 'Patch', 'All'].includes(decoratorName);
}
return false;
});
}
return false;
}
/**
* Check if the endpoint has a path parameter ID
*/
function hasPathParameterId(node) {
// Express.js style: app.get('/users/:id', ...)
if (node.type === 'CallExpression') {
const firstArg = node.arguments[0];
if (firstArg?.type === 'Literal' && typeof firstArg.value === 'string') {
return firstArg.value.includes(':id') ||
firstArg.value.includes(':uuid') ||
firstArg.value.match(/:[\w]*id/i);
}
}
// NestJS style: @Get(':id')
if (node.decorators) {
return node.decorators.some(decorator => {
if (decorator.expression.type === 'CallExpression') {
const firstArg = decorator.expression.arguments[0];
if (firstArg?.type === 'Literal' && typeof firstArg.value === 'string') {
return firstArg.value.includes(':id') ||
firstArg.value.includes(':uuid') ||
firstArg.value.match(/:[\w]*id/i);
}
}
return false;
});
}
// Check function parameters
if (node.params) {
return node.params.some(param => {
if (param.type === 'Identifier') {
const paramName = param.name.toLowerCase();
return paramName.includes('id') || paramName === 'uuid';
}
// Destructured parameter: { id }
if (param.type === 'ObjectPattern') {
return param.properties.some(prop => {
if (prop.type === 'Property' && prop.key.type === 'Identifier') {
const propName = prop.key.name.toLowerCase();
return propName.includes('id') || propName === 'uuid';
}
return false;
});
}
return false;
});
}
return false;
}
/**
* Check if the endpoint has security middleware
*/
function hasSecurityMiddleware(node) {
if (node.type !== 'CallExpression') return false;
// Check for middleware functions like auth, authenticate, authorize
return node.arguments.some(arg => {
if (arg.type === 'Identifier') {
const argName = arg.name.toLowerCase();
return argName.includes('auth') ||
argName.includes('guard') ||
argName.includes('protect') ||
argName.includes('secure') ||
argName.includes('jwt') ||
argName.includes('token');
}
return false;
});
}
/**
* Check if the method has custom query
*/
function hasCustomQuery(node) {
if (!node.body || node.body.type !== 'BlockStatement') return false;
return node.body.body.some(statement => {
const statementText = context.getSourceCode().getText(statement).toLowerCase();
return statementText.includes('query') ||
statementText.includes('sql') ||
statementText.includes('select') ||
statementText.includes('from') ||
statementText.includes('where') ||
statementText.includes('findby') ||
statementText.includes('createquerybuilder') ||
statementText.includes('rawquery');
});
}
/**
* Check if query contains user constraint
*/
function hasUserConstraintInQuery(node) {
if (!node.body || node.body.type !== 'BlockStatement') return false;
return node.body.body.some(statement => {
const statementText = context.getSourceCode().getText(statement).toLowerCase();
return statementText.includes('user_id') ||
statementText.includes('owner_id') ||
statementText.includes('created_by') ||
statementText.includes('userid') ||
statementText.includes('ownerid') ||
statementText.includes('createdby') ||
(statementText.includes('where') &&
(statementText.includes('user') || statementText.includes('owner')));
});
}
/**
* Check Repository query missing user/owner constraint
*/
function checkRepositorySecurityConstraint(node, context) {
if (!isRepositoryMethod(node)) return;
const hasCustomQueryResult = hasCustomQuery(node);
const hasUserConstraint = hasUserConstraintInQuery(node);
if (hasCustomQueryResult && !hasUserConstraint) {
context.report({
node,
messageId: 'repositoryMissingConstraint',
});
}
}
/**
* Check if method returns Entity directly instead of DTO
*/
function checkDirectEntityReturn(node, context) {
if (!isRestEndpoint(node)) return;
const returnsEntity = returnsEntityDirectly(node);
if (returnsEntity) {
context.report({
node,
messageId: 'directEntityReturn',
});
}
}
/**
* Check if ID field uses sequential number
*/
function checkSequentialIdUsage(node, context) {
const isEntityId = isEntityIdField(node);
const isSequentialType = isSequentialIdType(node);
if (isEntityId && isSequentialType) {
context.report({
node,
messageId: 'sequentialIdUsage',
});
}
}
/**
* Check direct repository access without authorization check
*/
function checkDirectRepositoryAccess(node, context) {
if (!isRepositoryFindMethod(node)) return;
const enclosingFunction = getEnclosingFunction(node);
if (!enclosingFunction) return;
const isInRestControllerResult = isInRestController(enclosingFunction);
const hasIdParameter = hasPathParameterId(enclosingFunction);
const hasAuthCheck = hasAuthorizationLogicNearby(node, enclosingFunction);
if (isInRestControllerResult && hasIdParameter && !hasAuthCheck) {
context.report({
node,
messageId: 'directRepositoryAccess',
});
}
}
// =================== HELPER METHODS ===================
/**
* Check if the call expression is a REST endpoint
*/
function isRestEndpoint(node) {
// Express.js style: app.get(), router.post(), etc.
if (node.type === 'CallExpression') {
const callee = node.callee;
if (callee.type === 'MemberExpression') {
const property = callee.property;
if (property.type === 'Identifier') {
const methodName = property.name;
return ['get', 'post', 'put', 'delete', 'patch', 'use', 'all'].includes(methodName);
}
}
}
// NestJS style: @Get(), @Post(), etc.
if (node.decorators) {
return node.decorators.some(decorator => {
if (decorator.expression.type === 'Identifier') {
const decoratorName = decorator.expression.name;
return ['Get', 'Post', 'Put', 'Delete', 'Patch', 'All'].includes(decoratorName);
}
if (decorator.expression.type === 'CallExpression' &&
decorator.expression.callee.type === 'Identifier') {
const decoratorName = decorator.expression.callee.name;
return ['Get', 'Post', 'Put', 'Delete', 'Patch', 'All'].includes(decoratorName);
}
return false;
});
}
return false;
}
/**
* Check if the endpoint has a path parameter ID
*/
function hasPathParameterId(node) {
// Express.js style: app.get('/users/:id', ...)
if (node.type === 'CallExpression') {
const firstArg = node.arguments[0];
if (firstArg?.type === 'Literal' && typeof firstArg.value === 'string') {
return firstArg.value.includes(':id') ||
firstArg.value.includes(':uuid') ||
firstArg.value.match(/:[\w]*id/i);
}
}
// NestJS style: @Get(':id')
if (node.decorators) {
return node.decorators.some(decorator => {
if (decorator.expression.type === 'CallExpression') {
const firstArg = decorator.expression.arguments[0];
if (firstArg?.type === 'Literal' && typeof firstArg.value === 'string') {
return firstArg.value.includes(':id') ||
firstArg.value.includes(':uuid') ||
firstArg.value.match(/:[\w]*id/i);
}
}
return false;
});
}
// Check function parameters
if (node.params) {
return node.params.some(param => {
if (param.type === 'Identifier') {
const paramName = param.name.toLowerCase();
return paramName.includes('id') || paramName === 'uuid';
}
// Destructured parameter: { id }
if (param.type === 'ObjectPattern') {
return param.properties.some(prop => {
if (prop.type === 'Property' && prop.key.type === 'Identifier') {
const propName = prop.key.name.toLowerCase();
return propName.includes('id') || propName === 'uuid';
}
return false;
});
}
return false;
});
}
return false;
}
/**
* Check if the endpoint has security middleware
*/
function hasSecurityMiddleware(node) {
if (node.type !== 'CallExpression') return false;
// Check for middleware functions like auth, authenticate, authorize
return node.arguments.some(arg => {
if (arg.type === 'Identifier') {
const argName = arg.name.toLowerCase();
return argName.includes('auth') ||
argName.includes('guard') ||
argName.includes('protect') ||
argName.includes('secure') ||
argName.includes('jwt') ||
argName.includes('token');
}
return false;
});
}
/**
* Check if the method has custom query
*/
function hasCustomQuery(node) {
if (!node.body || node.body.type !== 'BlockStatement') return false;
return node.body.body.some(statement => {
const statementText = context.getSourceCode().getText(statement).toLowerCase();
return statementText.includes('query') ||
statementText.includes('sql') ||
statementText.includes('select') ||
statementText.includes('from') ||
statementText.includes('where') ||
statementText.includes('findby') ||
statementText.includes('createquerybuilder') ||
statementText.includes('rawquery');
});
}
/**
* Check if query contains user constraint
*/
function hasUserConstraintInQuery(node) {
if (!node.body || node.body.type !== 'BlockStatement') return false;
return node.body.body.some(statement => {
const statementText = context.getSourceCode().getText(statement).toLowerCase();
return statementText.includes('user_id') ||
statementText.includes('owner_id') ||
statementText.includes('created_by') ||
statementText.includes('userid') ||
statementText.includes('ownerid') ||
statementText.includes('createdby') ||
(statementText.includes('where') &&
(statementText.includes('user') || statementText.includes('owner')));
});
}
/**
* Check Repository query missing user/owner constraint
*/
function checkRepositorySecurityConstraint(node, context) {
if (!isRepositoryMethod(node)) return;
const hasCustomQueryResult = hasCustomQuery(node);
const hasUserConstraint = hasUserConstraintInQuery(node);
if (hasCustomQueryResult && !hasUserConstraint) {
context.report({
node,
messageId: 'repositoryMissingConstraint',
});
}
}
/**
* Check if method returns Entity directly instead of DTO
*/
function checkDirectEntityReturn(node, context) {
if (!isRestEndpoint(node)) return;
const returnsEntity = returnsEntityDirectly(node);
if (returnsEntity) {
context.report({
node,
messageId: 'directEntityReturn',
});
}
}
/**
* Check if ID field uses sequential number
*/
function checkSequentialIdUsage(node, context) {
const isEntityId = isEntityIdField(node);
const isSequentialType = isSequentialIdType(node);
if (isEntityId && isSequentialType) {
context.report({
node,
messageId: 'sequentialIdUsage',
});
}
}
/**
* Check direct repository access without authorization check
*/
function checkDirectRepositoryAccess(node, context) {
if (!isRepositoryFindMethod(node)) return;
const enclosingFunction = getEnclosingFunction(node);
if (!enclosingFunction) return;
const isInRestControllerResult = isInRestController(enclosingFunction);
const hasIdParameter = hasPathParameterId(enclosingFunction);
const hasAuthCheck = hasAuthorizationLogicNearby(node, enclosingFunction);
if (isInRestControllerResult && hasIdParameter && !hasAuthCheck) {
context.report({
node,
messageId: 'directRepositoryAccess',
});
}
}
// =================== HELPER METHODS ===================
/**
* Check if the method is a Repository method
*/
function isRepositoryMethod(node) {
const className = getEnclosingClassName(node);
if (className) {
const lowerClassName = className.toLowerCase();
return lowerClassName.includes('repository') ||
lowerClassName.includes('service') ||
lowerClassName.includes('dao') ||
lowerClassName.includes('model');
}
// Check for repository-related decorators
if (node.decorators) {
return node.decorators.some(decorator => {
if (decorator.expression.type === 'Identifier') {
const decoratorName = decorator.expression.name.toLowerCase();
return decoratorName.includes('repository') ||
decoratorName.includes('injectable') ||
decoratorName.includes('service');
}
return false;
});
}
return false;
}
/**
* Check if the method returns Entity directly
*/
function returnsEntityDirectly(node) {
// Check TypeScript return type annotation
if (node.returnType) {
const returnTypeText = context.getSourceCode().getText(node.returnType).toLowerCase();
// Exclude safe types
if (returnTypeText.includes('dto') ||
returnTypeText.includes('response') ||
returnTypeText.includes('view') ||
returnTypeText.includes('model') ||
returnTypeText.includes('string') ||
returnTypeText.includes('number') ||
returnTypeText.includes('boolean') ||
returnTypeText.includes('void') ||
returnTypeText.includes('promise') ||
returnTypeText.includes('observable')) {
return false;
}
// Check if it's likely an Entity type
return isLikelyEntityType(returnTypeText);
}
// Check return statements
if (node.body && node.body.type === 'BlockStatement') {
return node.body.body.some(statement => {
if (statement.type === 'ReturnStatement' && statement.argument) {
const returnText = context.getSourceCode().getText(statement.argument);
return !returnText.includes('dto') &&
!returnText.includes('response') &&
!returnText.includes('view');
}
return false;
});
}
return false;
}
/**
* Check if the type is likely an Entity
*/
function isLikelyEntityType(returnType) {
// Entity usually starts with uppercase and is not a primitive type
const cleanType = returnType.replace(/[<>[\]{}]/g, '');
return /^[A-Z][a-zA-Z]*$/.test(cleanType) &&
!['Boolean', 'Number', 'String', 'Date', 'Array', 'Object', 'Promise', 'Observable'].includes(cleanType);
}
/**
* Check if the variable is an Entity ID field
*/
function isEntityIdField(node) {
let name;
if (node.type === 'VariableDeclarator' && node.id.type === 'Identifier') {
name = node.id.name.toLowerCase();
} else if (node.type === 'PropertyDefinition' && node.key.type === 'Identifier') {
name = node.key.name.toLowerCase();
} else {
return false;
}
return name === 'id' || name === '_id' || name.endsWith('id');
}
/**
* Check if the ID type is sequential
*/
function isSequentialIdType(node) {
// Check TypeScript type annotation
if (node.typeAnnotation) {
const typeText = context.getSourceCode().getText(node.typeAnnotation).toLowerCase();
return typeText.includes('number') || typeText.includes('int');
}
// Check initializer value
if (node.init && node.init.type === 'Literal') {
return typeof node.init.value === 'number';
}
return false;
}
/**
* Check if the method invocation is a repository find method
*/
function isRepositoryFindMethod(node) {
if (node.type !== 'CallExpression') return false;
if (node.callee.type === 'MemberExpression') {
const property = node.callee.property;
if (property.type === 'Identifier') {
const methodName = property.name.toLowerCase();
return methodName.startsWith('findby') ||
methodName.startsWith('getby') ||
methodName.startsWith('find') ||
methodName.startsWith('get') ||
methodName.startsWith('delete') ||
methodName.includes('id');
}
}
return false;
}
/**
* Check if the method is in a RestController
*/
function isInRestController(node) {
const className = getEnclosingClassName(node);
if (className) {
const lowerClassName = className.toLowerCase();
return lowerClassName.includes('controller') ||
lowerClassName.includes('route') ||
lowerClassName.includes('handler');
}
return false;
}
/**
* Check if there is authorization logic near the repository call
*/
function hasAuthorizationLogicNearby(repoCall, method) {
const enclosingFunction = getEnclosingFunction(repoCall);
if (!enclosingFunction || !enclosingFunction.body) return false;
const bodyNode = enclosingFunction.body;
if (bodyNode.type !== 'BlockStatement') return false;
return bodyNode.body.some(statement => {
const stmtText = context.getSourceCode().getText(statement).toLowerCase();
return containsSecurityCheck(stmtText);
});
}
/**
* Check if text contains security-related keywords
*/
function containsSecurityCheck(text) {
return text.includes('getcurrentuser') ||
text.includes('req.user') ||
text.includes('request.user') ||
text.includes('authentication') ||
text.includes('checkowner') ||
text.includes('haspermission') ||
text.includes('canaccess') ||
text.includes('isowner') ||
text.includes('validateaccess') ||
text.includes('authorize') ||
text.includes('jwt') ||
text.includes('token') ||
text.includes('permission') ||
text.includes('role') ||
text.includes('guard');
}
/**
* Check if the function has manual security check
*/
function hasManualSecurityCheck(node) {
let bodyNode;
if (node.type === 'CallExpression') {
// Check callback function in Express.js
const callback = node.arguments.find(arg =>
arg.type === 'FunctionExpression' ||
arg.type === 'ArrowFunctionExpression'
);
if (callback && callback.body) {
bodyNode = callback.body;
}
} else if (node.body) {
bodyNode = node.body;
}
if (!bodyNode) return false;
// If arrow function with expression body
if (bodyNode.type !== 'BlockStatement') {
const bodyText = context.getSourceCode().getText(bodyNode).toLowerCase();
return containsSecurityCheck(bodyText);
}
// Check block statement
return bodyNode.body.some(statement => {
const statementText = context.getSourceCode().getText(statement).toLowerCase();
return containsSecurityCheck(statementText);
});
}
// =================== UTILITY METHODS ===================
/**
* Get enclosing function of a node
*/
function getEnclosingFunction(node) {
let parent = node.parent;
while (parent) {
if (parent.type === 'FunctionDeclaration' ||
parent.type === 'MethodDefinition' ||
parent.type === 'FunctionExpression' ||
parent.type === 'ArrowFunctionExpression') {
return parent;
}
parent = parent.parent;
}
return null;
}
/**
* Get enclosing class name
*/
function getEnclosingClassName(node) {
let parent = node.parent;
while (parent) {
if (parent.type === 'ClassDeclaration' && parent.id) {
return parent.id.name;
}
parent = parent.parent;
}
return null;
}
}
};